# `StorageBackend` _(no submodules)_ ## Purpose `StorageBackend` adapts any `AbstractStorage` instance from `@suaveplan/storage` into a translation `Backend` for the `@suaveplan/i18n` family. This is the headline persistence integration that lets translations live in S3, OPFS, IndexedDB, SQLite, Garage, or any other storage `@suaveplan/storage` supports — without each consumer integrating once per source. The user- facing pitch is "translations in OPFS for offline" or "translations on a content-addressed CDN" become one config line. The backend has zero runtime dependencies on `@suaveplan/i18n` core. It implements the full `Backend` contract from `@suaveplan/types` (`read`, `readMulti`, `save`, `lastModified`) and emits OpenTelemetry instruments under the `i18n.backend.storage.*` prefix so production teams can dashboard latency and miss-rate per layout. ## Features - Full 5-flag capability matrix: `canRead`, `canReadMulti`, `canSave`, `canTrackModification` all `true`; `canWatch: false`. - Three wire layouts: `single-key-per-namespace` (default), `single-key-per-locale`, `flat-keys`. - Pluggable codec — JSON by default; swap for MessagePack, CBOR, or schema-validated JSON. - Optional `signal?: AbortSignal` parameter on `read`, `readMulti`, and `save`; pre-aborted signals throw `I18nError(I18N.BACKEND_TIMEOUT)` without touching the underlying storage. - OTel histograms + counters emitted on every read/save with a `layout` attribute so per-layout latency can be charted. - Zod boundary validation: invalid options throw `I18nError(I18N.INVALID_OPTIONS)` at construction time. - `BACKEND_LOCALE_NOT_AVAILABLE` on miss (chained backends fall through); `BACKEND_READ_FAILED` / `BACKEND_WRITE_FAILED` on transport / codec errors (chained backends propagate without fallthrough). ## Layouts ### `single-key-per-namespace` (default) ```text i18n/en/auth.json → {"login":{"title":"Sign in"}} i18n/en/common.json → {"cancel":"Cancel"} ``` One storage key per `(locale, namespace)`. Reads are exactly one storage `get` per `read(locale, ns)` call. Best for HTTP-CDN-style storages where each key is independently cacheable. ### `single-key-per-locale` ```text i18n/en.json → {"auth":{...},"common":{...}} ``` One storage key per locale, holding the entire `Record` map. Best when namespaces are small and read together. Saving a single namespace requires a read-modify-write of the whole locale map. ### `flat-keys` ```text i18n:en:auth:login.title → Sign in i18n:en:common:cancel → Cancel ``` One storage key per leaf string. Best for content-addressed storages where deltas are expensive at namespace granularity but cheap at leaf granularity. ## Usage: Basic Example ```ts import { StorageBackend } from "@suaveplan/i18n-backend-storage"; import { MemoryStorage } from "@suaveplan/storage"; const backend = new StorageBackend({ storage: new MemoryStorage() }); await backend.save("en", "auth", { login: { title: "Sign in" } }); const record = await backend.read("en", "auth"); // → { login: { title: "Sign in" } } ``` ## Usage: Advanced Example — OPFS + custom binary codec ```ts import { StorageBackend } from "@suaveplan/i18n-backend-storage"; import { OpfsStorage } from "@suaveplan/storage-browser-opfs"; import { encode, decode } from "@msgpack/msgpack"; const backend = new StorageBackend({ storage: new OpfsStorage({ rootDir: "i18n" }), layout: "single-key-per-locale", encode: (record) => encode(record), decode: (bytes) => decode(bytes) as Record, }); // Cancellable reads play nicely with multi-backend orchestration. const controller = new AbortController(); controller.abort(); await backend.read("en", "auth", controller.signal); // throws BACKEND_TIMEOUT ``` ## API Reference ### `class StorageBackend implements Backend` - `constructor(options: StorageBackendOptions)` — validates options via `StorageBackendOptionsSchema`; throws `I18nError(I18N.INVALID_OPTIONS)` on schema mismatch or missing `storage`. - `name: string` — backend label (default `"storage"`). - `capabilities: BackendCapabilities` — frozen 5-flag matrix. - `read(locale, namespace, signal?): Promise` — resolves with the stored record; throws `I18nError(I18N.BACKEND_LOCALE_NOT_AVAILABLE)` on miss. - `readMulti(locale, namespaces, signal?): Promise>` — returns only the namespaces that exist; missing namespaces are omitted. - `save(locale, namespace, record, signal?): Promise` — persists the record; failure surfaces as `BACKEND_WRITE_FAILED`. - `lastModified(locale, namespace): Promise` — returns the underlying storage's `modifiedAt` field, or `undefined` if metadata is unavailable. ### `defaultCodec: StorageBackendCodec` JSON-based codec (`TextEncoder` / `JSON.stringify`). The decode path rejects non-object payloads (strings, arrays, null) as `BACKEND_READ_FAILED`. ## Implementation Notes - The default JSON codec uses `TextEncoder` / `TextDecoder` and `JSON.stringify` / `JSON.parse`. The decode path rejects non- object payloads because translation records are always objects in the contract; custom codecs MUST also reject non-objects or document the relaxation explicitly. - `flat-keys` reads via `AbstractStorage.list({ prefix })`. List is required by every concrete storage, so the layout works against all of them. Saves write each leaf with its own `set` call — for backends that batch writes, prefer `single-key-per-namespace` to amortise the batch. - `lastModified` for `flat-keys` takes the maximum `modifiedAt` across leaves. For other layouts it consults the namespace or locale key directly. All paths swallow storage exceptions and return `undefined` rather than throw, mirroring the contract that `lastModified` is a hint, not a guarantee. - `signal?.aborted` is checked at the entry of every async method. The throw surfaces as `I18nError(I18N.BACKEND_TIMEOUT)`. The flag is sampled exactly once at the top — once the underlying `AbstractStorage` operation begins, abort propagation is the storage's responsibility. - OTel `recordRead` / `recordWrite` calls emit zero overhead unless a `MeterProvider` is registered. Tests use the in-memory metrics state ring exposed by `./observability` to assert payloads without standing up the full SDK. The `layout` attribute is a three-value low-cardinality enum.