# `@suaveplan/i18n-backend-storage` — AbstractStorage-backed translation backend **Package**: `@suaveplan/i18n-backend-storage` (NEW) **Status**: PROPOSED **Type**: feature (new universal-tier package; verbatim port from monolith) **Date**: 2026-05-05 **Wave**: 5 **Design doc**: `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md` **Path**: `packages/i18n/i18n-backend-storage/` ## Why The Wave 5 backend split lifts each `Backend` implementation from the `@suaveplan/i18n` monolith. The storage backend bridges the `Backend` plugin contract to any `AbstractStorage` implementation — `MemoryStorage`, `OPFSStorage`, `IndexedDBStorage`, `S3Storage`, `NodeFsStorage`, `SqliteStorage`, `GarageStorage`. It is the canonical persistence layer for browser-tier consumers (offline-first via OPFS / IndexedDB), for server-tier consumers (Garage / S3 in production), and for fixture-driven tests (`MemoryStorage`). A key reason for splitting this backend out is **dependency isolation**: today `@suaveplan/storage` is a hard transitive dep of every `@suaveplan/i18n` consumer just because of this one backend. After the split, only consumers who explicitly install `@suaveplan/i18n-backend-storage` pull `@suaveplan/storage`. Browser-tier apps using only the HTTP backend save the install cost. The `Backend` interface itself moves to `@suaveplan/types/i18n` in Wave 1. `@suaveplan/i18n-backend-storage` implements `Backend` from that location and does NOT depend on `@suaveplan/i18n` core. Runtime atoms come from `@suaveplan/i18n-shared`. ## What changes ### New package: `@suaveplan/i18n-backend-storage` - ADDED: `packages/i18n/i18n-backend-storage/` package scaffold (package.json, tsconfig.json, vite.config.ts, README.md). - ADDED: `src/backend.ts` — `StorageBackend` class implementing `Backend` from `@suaveplan/types/i18n`. Source: `packages/i18n/i18n/src/core/backend/storage/storage-backend.ts` L75-L359. Capabilities: `canRead: true`, `canReadMulti: true`, `canSave: true`, `canTrackModification: true`, `canWatch: false`. Three wire layouts: `single-key-per-namespace` (default), `single-key-per-locale`, `flat-keys`. - ADDED: `src/backend.types.ts` — `StorageBackendOptions`, `StorageBackendLayout`, `StorageBackendCodec`. Source: monolith L12-L34. - ADDED: `src/backend.schemas.ts` — `StorageBackendOptionsSchema` Zod schema. Source: monolith `storage-backend.schemas.ts` (25 lines). - ADDED: `src/factory.ts` — `storageBackend(options: StorageBackendOptions): StorageBackend` factory. Source: monolith L403-L405. - ADDED: `src/codec.ts` — `defaultCodec` (JSON via `TextEncoder` / `TextDecoder`) lifted as a standalone export so consumers can compose custom codecs. Source: monolith L36-L56. - ADDED: `src/layout-helpers.ts` — `flattenLeaves`, `assignAtDotPath`, `defaultPrefix` lifted as standalone helpers. Source: monolith L361-L400. - ADDED: `src/observability.ts` — emits `i18n.backend.storage.read_duration_ms`, `i18n.backend.storage.read_total{outcome,layout}`, `i18n.backend.storage.read_errors_total{code,layout}`, `i18n.backend.storage.write_duration_ms`, `i18n.backend.storage.write_total{outcome,layout}`. The `layout` attribute is a low-cardinality enum ("single-key-per-namespace" | "single-key-per-locale" | "flat-keys"). - ADDED: `src/backend.test.ts`, `src/backend.spec-conformance.test.ts`, `src/factory.test.ts`, `src/codec.test.ts`, `src/layout-helpers.test.ts`, `src/index.test.ts` — Vitest suites verbatim-ported from monolith plus new spec-conformance suites. - ADDED: `src/backend.md`, `src/factory.md`, `src/codec.md`, `src/layout-helpers.md` — co-located docs. - ADDED: `src/index.ts` — explicit named re-exports. ### Subpath exports - `.` — root barrel - `./backend` — `StorageBackend` class only - `./factory` — `storageBackend` factory function only - `./codec` — `defaultCodec` + `StorageBackendCodec` type only - `./layout-helpers` — `flattenLeaves`, `assignAtDotPath`, `defaultPrefix` only ### Dependencies - `@suaveplan/i18n-shared workspace:^` — for `I18N_CODES`, `I18nError` - `@suaveplan/types workspace:^` — for `Backend`, `BackendCapabilities`, `TranslationRecord` from `@suaveplan/types/i18n` - `@suaveplan/storage workspace:^` — for `AbstractStorage` interface (this is the one extra workspace dep that distinguishes storage from the other universal backends) - `@suaveplan/error workspace:^` - `@opentelemetry/api catalog:observability` - `tslib catalog:build` - `zod catalog:forms` — for `StorageBackendOptionsSchema` - **No** dep on `@suaveplan/i18n` core. **No** dep on any other backend package. ### Tier Universal. `AbstractStorage` is itself a universal abstraction — concrete storages decide their tier (e.g. `OPFSStorage` is browser-only, `NodeFsStorage` is server-only, `MemoryStorage` is universal). The backend itself is tier-neutral. ## Out of scope - **Removing the storage backend from `packages/i18n/i18n/src/core/backend/storage/`** — happens in Wave 6. - **Encryption-at-rest layer** — out of scope; consumers wire `EncryptedStorage` from a future package or rely on the storage implementation. - **Watch / change-notification** — `canWatch: false` is intentional; the underlying `AbstractStorage` does not currently surface change events. - **Schema evolution** — `flat-keys` layout has no built-in schema migration. Consumers handle versioning at the catalog layer. ## Source mapping | Requirement | Source file | Source lines | Notes | |---|---|---|---| | `StorageBackend` class + capabilities matrix | `packages/i18n/i18n/src/core/backend/storage/storage-backend.ts` | L75-L113 | Verbatim port; rewire imports | | `StorageBackend.read` (layout dispatch + outcome telemetry) | same file | L115-L142 | Verbatim port | | `#readInternal` (per-layout dispatch) | same file | L144-L179 | Verbatim port | | `StorageBackend.readMulti` | same file | L181-L216 | Verbatim port | | `StorageBackend.save` | same file | L218-L247 | Verbatim port | | `StorageBackend.lastModified` | same file | L249-L275 | Verbatim port | | Layout key helpers (`#nsKey`, `#localeKey`) | same file | L279-L285 | Inline private methods preserved | | `#readLocaleMap` | same file | L287-L308 | Verbatim port | | `#readFlatKeys` | same file | L310-L327 | Verbatim port | | `#saveFlatKeys` | same file | L329-L343 | Verbatim port | | `#missing` helper (constructs `BACKEND_LOCALE_NOT_AVAILABLE`) | same file | L345-L358 | Verbatim port | | `defaultCodec` (JSON encode/decode) | same file | L36-L56 | Lifted to `src/codec.ts` | | `defaultPrefix` | same file | L361-L363 | Lifted to `src/layout-helpers.ts` | | `flattenLeaves` | same file | L365-L379 | Lifted to `src/layout-helpers.ts` | | `assignAtDotPath` | same file | L381-L400 | Lifted to `src/layout-helpers.ts` | | `StorageBackendOptions`, `StorageBackendLayout`, `StorageBackendCodec` types | same file | L12-L34 | Verbatim port to `backend.types.ts` | | `storageBackend` factory | same file | L403-L405 | Lifted to `factory.ts` | | `StorageBackendOptionsSchema` | `packages/i18n/i18n/src/core/backend/storage/storage-backend.schemas.ts` | full file (25 lines) | Verbatim port | | `recordRead` / `recordWrite` instrumentation pattern | `packages/i18n/i18n/src/observability/backend-metrics.ts` | full file | Reimplemented inline; metric names re-prefixed `i18n.backend.storage.`; `layout` attribute added | ### Requirements with no source line | Requirement | Justification | |---|---| | Subpath exports `./backend`, `./factory`, `./codec`, `./layout-helpers` | Tree-shaking — codec and layout helpers are reusable in custom backend implementations | | `AbortSignal` parameter on `read` / `readMulti` / `save` | Cancellation parity across backends | | `layout` attribute on metrics | Operability — production teams need per-layout latency / error breakdowns | ## Verification approach Each requirement carries a `**Verified by:**` line in the spec delta. Concrete test files: - `packages/i18n/i18n-backend-storage/src/backend.test.ts` — verbatim port of monolith `storage-backend.test.ts` (582 lines); covers all three layouts (`single-key-per-namespace`, `single-key-per-locale`, `flat-keys`), `read` hit/miss, `readMulti` partial-record, `save` round-trip per layout, `lastModified` per layout, codec injection, prefix override, `BACKEND_LOCALE_NOT_AVAILABLE` on miss, `BACKEND_WRITE_FAILED` on storage failure - `packages/i18n/i18n-backend-storage/src/backend.spec-conformance.test.ts` — verbatim port of monolith `storage-backend.spec-conformance.test.ts` (94 lines) - `packages/i18n/i18n-backend-storage/src/factory.test.ts` — `storageBackend()` returns `StorageBackend` instance; defaults `name` to `"storage"` - `packages/i18n/i18n-backend-storage/src/codec.test.ts` — `defaultCodec` round-trip; rejects non-object decoded payloads with `BACKEND_READ_FAILED` - `packages/i18n/i18n-backend-storage/src/layout-helpers.test.ts` — `flattenLeaves` produces dot-paths; `assignAtDotPath` round-trips; `defaultPrefix` returns `"i18n"` for both layouts - `packages/i18n/i18n-backend-storage/src/index.test.ts` — every documented public export resolves through every documented subpath ## Impact - New universal-tier package — joins the i18n family under `packages/i18n/`. Pulls `@suaveplan/storage` only when the consumer installs this package. - `@suaveplan/i18n` slim core's transitive dependency on `@suaveplan/storage` is eliminated for HTTP-only / memory-only consumers. - No downstream consumer changes needed in Wave 5 — the slim core (Wave 6) re-exports `StorageBackend` and `storageBackend`. - `MANIFEST.md` regenerated; `CLAUDE.md` package listing updated.