# @suaveplan/i18n-backend-storage - Universal translation backend backed by any AbstractStorage from @suaveplan/storage. Three wire layouts: single-key-per-namespace (default), single-key-per-locale, flat-keys. Pluggable codec, AbortSignal cancellation, OpenTelemetry instrumentation under i18n.backend.storage.*. Zero runtime dependencies on @suaveplan/i18n core. ## Summary Universal translation backend for the `@suaveplan/i18n` family, backed by any `AbstractStorage` implementation from `@suaveplan/storage`. Implements the full `Backend` contract (`read`, `readMulti`, `save`, `lastModified`) over three wire layouts: `single-key-per-namespace` (default), `single-key-per-locale`, and `flat-keys`. Pluggable codec, optional `AbortSignal` cancellation, OpenTelemetry instrumentation under the `i18n.backend.storage.*` prefix. Zero runtime dependencies on `@suaveplan/i18n` core. ## Documentation tree - [backend](./src/backend/backend.md) — _(no submodules)_ - [backend](./src/backend/backend.md) — _(no submodules)_ - [factory](./src/factory/factory.md) — _(no submodules)_ - [factory](./src/factory/factory.md) — _(no submodules)_ - [observability](./src/observability/observability.md) — _(no submodules)_ - [observability](./src/observability/observability.md) — _(no submodules)_ - [backend](./src/backend/backend.md) — _(no submodules)_- [factory](./src/factory/factory.md) — _(no submodules)_- [factory](./src/factory/factory.md) — _(no submodules)_- [observability](./src/observability/observability.md) — _(no submodules)_- [observability](./src/observability/observability.md) — _(no submodules)_- [backend](./src/backend/backend.md) — _(no submodules)_- [factory](./src/factory/factory.md) — _(no submodules)_- [factory](./src/factory/factory.md) — _(no submodules)_- [observability](./src/observability/observability.md) — _(no submodules)_- [observability](./src/observability/observability.md) — _(no submodules)_- [backend](./src/backend/backend.md) — _(no submodules)_- [factory](./src/factory/factory.md) — _(no submodules)_- [factory](./src/factory/factory.md) — _(no submodules)_- [observability](./src/observability/observability.md) — _(no submodules)_- [observability](./src/observability/observability.md) — _(no submodules)_- [backend](./src/backend/backend.md) — _(no submodules)_- [factory](./src/factory/factory.md) — _(no submodules)_ - [factory](./src/factory/factory.md) — _(no submodules)_- [observability](./src/observability/observability.md) — _(no submodules)_ - [observability](./src/observability/observability.md) — _(no submodules)_ ## Why this exists The Wave 5 modular split lifts each backend out of the `@suaveplan/i18n` monolith into its own sibling so consumers can install only the backends they need. The storage backend is the canonical persistence layer: pair it with `MemoryStorage` for tests, `OpfsStorage` / `IndexedDBStorage` for offline-first browser apps, or `S3Storage` / `GarageStorage` / `NodeFsStorage` / `SqliteStorage` for server-tier deployments. Splitting it out also isolates `@suaveplan/storage` as a transitive dep — consumers using only the HTTP or memory backends no longer pull it in. ## Install ```bash bun add @suaveplan/i18n-backend-storage @suaveplan/storage ``` ## Quick Start ```ts import { storageBackend } from "@suaveplan/i18n-backend-storage"; import { MemoryStorage } from "@suaveplan/storage"; const backend = storageBackend({ storage: new MemoryStorage() }); await backend.save("en", "auth", { login: { title: "Sign in" } }); const record = await backend.read("en", "auth"); // → { login: { title: "Sign in" } } ``` ## Modules | Subpath | Description | |---|---| | `@suaveplan/i18n-backend-storage` | Root barrel — class + factory + types + schemas + observability | | `@suaveplan/i18n-backend-storage/backend` | `StorageBackend` class + `defaultCodec` | | `@suaveplan/i18n-backend-storage/factory` | `storageBackend` factory function only | | `@suaveplan/i18n-backend-storage/schemas` | Zod boundary schemas | | `@suaveplan/i18n-backend-storage/observability` | OTel instruments + in-memory state ring | ## Modules Documentation - [backend.md](src/backend/backend.md) — `StorageBackend` class, layouts, codec - [factory.md](src/factory/factory.md) — `storageBackend` factory function - [observability.md](src/observability/observability.md) — OTel instruments + cardinality ## Capabilities | Flag | Value | Notes | |---|---|---| | `canRead` | `true` | always | | `canReadMulti` | `true` | per-layout dispatch | | `canSave` | `true` | persists into the wrapped storage | | `canTrackModification` | `true` | consults `getMetadata().modifiedAt` | | `canWatch` | `false` | the storage contract has no change feed | ## Layouts - `single-key-per-namespace` (default) — one key per `(locale, ns)` at `//.json`. Best for HTTP-CDN-style storages. - `single-key-per-locale` — one key per locale holding the whole namespace map. Best when namespaces are small and read together. - `flat-keys` — one key per leaf string at `:::`. Best for content- addressed storages with cheap per-leaf delta shipping. ## Testing The package's own tests use the shared `runBackendConformance` harness from `@suaveplan/i18n-shared/testing` so the universal `Backend` contract is exercised against a fresh storage backend per assertion (against all three layouts): ```ts import { runBackendConformance } from "@suaveplan/i18n-shared/testing"; import { StorageBackend } from "@suaveplan/i18n-backend-storage"; import { MemoryStorage } from "@suaveplan/storage"; runBackendConformance("StorageBackend", async () => { const backend = new StorageBackend({ storage: new MemoryStorage() }); return { backend, async seed(locale, namespace, record) { await backend.save(locale, namespace, record as never); }, }; }); ``` ## Versioning This package is part of the i18n modular-split rollout. Stays at `0.1.0` until the slim core (Wave 6) cuts the family `1.0.0` release. The metric names under `i18n.backend.storage.*` are part of the public API contract and will not change without a major bump. ## Wave 5 Scope This package is Wave 5b of the 14-package i18n modular split. It ships pure additions only — `packages/i18n/i18n/src/core/backend/storage/` retains its copy of the backend until Wave 6 deletes it. Both copies coexist during Wave 5; downstream consumers continue to import from the monolith and observe no behaviour change.