Files
suaveplan/openspec/changes/archive/2026-05-04-i18n-storage-backends/tasks.md
2026-05-27 23:29:53 +01:00

8.6 KiB

Tasks — i18n storage-backed translation loaders

Every checkbox MUST reflect work actually done in this change. Do not retroactively tick.

1. Spec authoring

  • 1.1 Read openspec/specs/i18n/spec.md end-to-end (canonical capability spec)
  • 1.2 Read openspec/AGENTS.md Sections 13 and 14
  • 1.3 Read packages/storage/storage/src/abstract/abstract.ts to confirm the public surface this change consumes
  • 1.4 Confirm i18n-formatters and i18n-spec-conformance are landed (or land in parallel) before this change archives — the OTel meter wiring depends on them
  • 1.5 openspec change validate i18n-storage-backends exits with no errors

2. Backend interface and error codes

  • 2.1 Create packages/i18n/i18n/src/core/backend/backend.types.ts with Backend, BackendInitContext, LayoutOption, BackendCapabilities types
  • 2.2 Add I18N.BACKEND_READ_FAILED, I18N.BACKEND_WRITE_FAILED, I18N.BACKEND_NOT_REGISTERED, I18N.BACKEND_LOCALE_NOT_AVAILABLE to src/error-codes.ts via defineCodes
  • 2.3 Sibling backend.types.md (≥200 words, full interface walkthrough)
  • 2.4 Author the shared interface-conformance suite at src/core/backend/backend.test.ts (every concrete backend imports + runs it)

3. MemoryBackend

  • 3.1 Implement MemoryBackend at src/core/backend/memory/memory-backend.ts (≤200 lines hard cap)
  • 3.2 read, readMulti, save, lastModified — all four implemented
  • 3.3 seed constructor option for tests
  • 3.4 memory-backend.test.ts — seed read, save round-trip, lastModified monotonicity, readMulti merge
  • 3.5 Sibling .md

4. HttpBackend

  • 4.1 Implement HttpBackend at src/core/backend/http/http-backend.ts
  • 4.2 loadPath template with {locale} / {namespace} placeholders
  • 4.3 Injectable fetchFn (default globalThis.fetch), parsePayload, customHeaders, crossDomain
  • 4.4 Map HTTP 404 → BACKEND_LOCALE_NOT_AVAILABLE; HTTP 5xx + TypeError (network) → BACKEND_READ_FAILED
  • 4.5 Optional retry: RetryPolicy from @suaveplan/retry (peer; resolves automatically when present)
  • 4.6 HttpBackendOptionsSchema (Zod) at src/core/backend/http/http-backend.schema.ts
  • 4.7 http-backend.test.ts — templated URL, 404 mapping, 5xx mapping, parsePayload swap, custom headers callback
  • 4.8 Sibling .md

5. StorageBackend

  • 5.1 Add @suaveplan/storage workspace:^ to dependencies in packages/i18n/i18n/package.json
  • 5.2 Implement StorageBackend at src/core/backend/storage/storage-backend.ts
  • 5.3 Three layouts: single-key-per-namespace (default), single-key-per-locale, flat-keys
  • 5.4 Pluggable codec: encode(record): Uint8Array / decode(bytes): TranslationRecord; default JSON
  • 5.5 lastModified via AbstractStorage.getMetadata when available
  • 5.6 StorageBackendOptionsSchema (Zod)
  • 5.7 storage-backend.test.ts — all three layouts round-trip, codec swap to MessagePack, missing key surfaces BACKEND_LOCALE_NOT_AVAILABLE
  • 5.8 storage-backend.spec-conformance.test.ts — uses real MemoryStorage from @suaveplan/storage, asserts OTel read.duration_ms sample emission
  • 5.9 Sibling .md covering all three layouts with tradeoffs

6. MultiFileFsBackend

  • 6.1 Implement MultiFileFsBackend at src/core/backend/multi-file-fs/multi-file-fs-backend.ts
  • 6.2 Three layouts: nested (default), flat, glob
  • 6.3 Optional peer parsers: yaml, gettext-parser (PO), json5; auto-detect via dynamic import() and surface a clear BACKEND_READ_FAILED if peer is missing for a given file extension
  • 6.4 watch: true opt-in via node:fs/promises.watch; emit loaded event on file change
  • 6.5 MultiFileFsBackendOptionsSchema (Zod)
  • 6.6 multi-file-fs-backend.test.ts — nested + flat + glob layouts, missing file → BACKEND_LOCALE_NOT_AVAILABLE, watch-emits-loaded with a real temp dir
  • 6.7 Sibling .md documenting the "many JSON files in many places" use case directly
  • 6.8 Subpath export ./backend/multi-file-fs in package.json so browser builds tree-shake the Node-only backend

7. ChainedBackend

  • 7.1 Implement ChainedBackend at src/core/backend/chained/chained-backend.ts
  • 7.2 read walks in order; first success wins; subsequent backends skipped
  • 7.3 Cache write-through: after a downstream success, call save on every preceding backend that implements it
  • 7.4 readMulti fans out concurrently, merges per mergeStrategy: 'first-wins' | 'last-wins'
  • 7.5 BACKEND_READ_FAILED propagates without fallthrough; only BACKEND_LOCALE_NOT_AVAILABLE triggers fallthrough
  • 7.6 chained-backend.test.ts — short-circuit on cache hit, fallthrough on locale-not-available, no fallthrough on read-failed, mergeStrategy semantics
  • 7.7 chained-backend.spec-conformance.test.ts — write-through cache uses real MemoryBackend + stub HttpBackend; verifies exact call sequence
  • 7.8 Sibling .md

8. Catalog integration

  • 8.1 Extend MessageCatalog constructor to accept backend?: Backend or backends?: Backend[]
  • 8.2 Implement MessageCatalog.preload(locales, namespaces?) → Promise<void> with readMulti preference
  • 8.3 Implement MessageCatalog.invalidate(locale?, namespace?) → void
  • 8.4 Implement MessageCatalog.getAsync(key, locale?) → Promise<LookupResult> with in-flight load dedup
  • 8.5 Synchronous get / t MUST NOT trigger backend reads (verified by spec-conformance test)
  • 8.6 Implement saveMissing: true option — best-effort write-back via backend.save; OTel backend.write.total{outcome=error} increment on failure (no throw)
  • 8.7 Modify existing MessageCatalog.load(locale, namespace?, loader?) to make loader optional and route to backend when omitted
  • 8.8 catalog-backend.test.ts — preload, invalidate, getAsync, saveMissing, sync-get-never-triggers-backend
  • 8.9 Update catalog.md and root README.md ## Quick Start with a StorageBackend example

9. OTel instrumentation

  • 9.1 i18n.backend.read.duration_ms histogram emission wired in every concrete backend
  • 9.2 i18n.backend.read.total and i18n.backend.write.total counters wired in every concrete backend
  • 9.3 i18n.backend.cache.size observable gauge registered against MessageCatalog's internal Map.size
  • 9.4 OTel emission tested with real InMemoryMetricExporter in storage-backend.spec-conformance.test.ts and catalog-backend.test.ts

10. Wiring and exports

  • 10.1 Update src/index.ts to export Backend interface, all five backend factories, and MessageCatalog async surface
  • 10.2 Add subpath exports for every backend module in package.json: ./backend, ./backend/memory, ./backend/http, ./backend/storage, ./backend/multi-file-fs, ./backend/chained
  • 10.3 README ## Modules table lists every new co-located .md
  • 10.4 README ## Backends section with one example per backend; the MultiFileFsBackend example explicitly demonstrates the <locale>/<namespace>.json workflow

11. Pre-archive verification (the 5-gate triple)

  • 11.1 bun run check — Biome 0/0/0 across the monorepo
  • 11.2 bunx turbo lint typecheck test --filter=@suaveplan/i18n — green
  • 11.3 bunx turbo test:coverage --filter=@suaveplan/i18n — 100/100/100/100
  • 11.4 bun scripts/openspec-conformance.ts @suaveplan/i18n — exit 0, no error findings
  • 11.5 bunx turbo build --filter=@suaveplan/i18n — green

12. Pre-archive 7-row checklist (AGENTS.md Section 13)

  • 12.1 Generics threaded; expectTypeOf assertions on Backend.read return type narrowing for typed message trees
  • 12.2 Zod schemas wired at every public boundary (HttpBackendOptionsSchema, StorageBackendOptionsSchema, MultiFileFsBackendOptionsSchema, ChainedBackendOptionsSchema)
  • 12.3 All four new error codes thrown from at least one production path
  • 12.4 Strict-mode tested: getAsync rejection vs sync-get fallback paths
  • 12.5 OTel histograms / counters / gauge emitted from production paths and asserted with a real in-memory MeterProvider
  • 12.6 Lifecycle dedup: concurrent preload(en, [auth]) calls trigger one backend read; concurrent getAsync('auth:k') calls trigger one backend read
  • 12.7 No retroactive tick — every box reflects work done in this change

13. Archive

  • 13.1 Fold spec delta into openspec/specs/i18n/spec.md
  • 13.2 Move openspec/changes/i18n-storage-backends/ → openspec/archive/<yyyy-mm-dd>-i18n-storage-backends/
  • 13.3 Refresh MANIFEST.md if dep set changed (@suaveplan/storage is a new dep)