Files
2026-05-27 23:29:53 +01:00

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 — 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.