9.4 KiB
@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—StorageBackendclass implementingBackendfrom@suaveplan/types/i18n. Source:packages/i18n/i18n/src/core/backend/storage/storage-backend.tsL75-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—StorageBackendOptionsSchemaZod schema. Source: monolithstorage-backend.schemas.ts(25 lines). - ADDED:
src/factory.ts—storageBackend(options: StorageBackendOptions): StorageBackendfactory. Source: monolith L403-L405. - ADDED:
src/codec.ts—defaultCodec(JSON viaTextEncoder/TextDecoder) lifted as a standalone export so consumers can compose custom codecs. Source: monolith L36-L56. - ADDED:
src/layout-helpers.ts—flattenLeaves,assignAtDotPath,defaultPrefixlifted as standalone helpers. Source: monolith L361-L400. - ADDED:
src/observability.ts— emitsi18n.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}. Thelayoutattribute 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—StorageBackendclass only./factory—storageBackendfactory function only./codec—defaultCodec+StorageBackendCodectype only./layout-helpers—flattenLeaves,assignAtDotPath,defaultPrefixonly
Dependencies
@suaveplan/i18n-shared workspace:^— forI18N_CODES,I18nError@suaveplan/types workspace:^— forBackend,BackendCapabilities,TranslationRecordfrom@suaveplan/types/i18n@suaveplan/storage workspace:^— forAbstractStorageinterface (this is the one extra workspace dep that distinguishes storage from the other universal backends)@suaveplan/error workspace:^@opentelemetry/api catalog:observabilitytslib catalog:buildzod catalog:forms— forStorageBackendOptionsSchema- No dep on
@suaveplan/i18ncore. 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
EncryptedStoragefrom a future package or rely on the storage implementation. - Watch / change-notification —
canWatch: falseis intentional; the underlyingAbstractStoragedoes not currently surface change events. - Schema evolution —
flat-keyslayout 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 monolithstorage-backend.test.ts(582 lines); covers all three layouts (single-key-per-namespace,single-key-per-locale,flat-keys),readhit/miss,readMultipartial-record,saveround-trip per layout,lastModifiedper layout, codec injection, prefix override,BACKEND_LOCALE_NOT_AVAILABLEon miss,BACKEND_WRITE_FAILEDon storage failurepackages/i18n/i18n-backend-storage/src/backend.spec-conformance.test.ts— verbatim port of monolithstorage-backend.spec-conformance.test.ts(94 lines)packages/i18n/i18n-backend-storage/src/factory.test.ts—storageBackend()returnsStorageBackendinstance; defaultsnameto"storage"packages/i18n/i18n-backend-storage/src/codec.test.ts—defaultCodecround-trip; rejects non-object decoded payloads withBACKEND_READ_FAILEDpackages/i18n/i18n-backend-storage/src/layout-helpers.test.ts—flattenLeavesproduces dot-paths;assignAtDotPathround-trips;defaultPrefixreturns"i18n"for both layoutspackages/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/storageonly when the consumer installs this package. @suaveplan/i18nslim core's transitive dependency on@suaveplan/storageis eliminated for HTTP-only / memory-only consumers.- No downstream consumer changes needed in Wave 5 — the slim core (Wave 6) re-exports
StorageBackendandstorageBackend. MANIFEST.mdregenerated;CLAUDE.mdpackage listing updated.