# Tasks — i18n storage-backed translation loaders > Every checkbox MUST reflect work actually done in this change. Do not retroactively tick. ## 1. Spec authoring - [x] 1.1 Read `openspec/specs/i18n/spec.md` end-to-end (canonical capability spec) - [x] 1.2 Read `openspec/AGENTS.md` Sections 13 and 14 - [x] 1.3 Read `packages/storage/storage/src/abstract/abstract.ts` to confirm the public surface this change consumes - [x] 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 - [x] 1.5 `openspec change validate i18n-storage-backends` exits with no errors ## 2. Backend interface and error codes - [x] 2.1 Create `packages/i18n/i18n/src/core/backend/backend.types.ts` with `Backend`, `BackendInitContext`, `LayoutOption`, `BackendCapabilities` types - [x] 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` - [x] 2.3 Sibling `backend.types.md` (≥200 words, full interface walkthrough) - [x] 2.4 Author the shared interface-conformance suite at `src/core/backend/backend.test.ts` (every concrete backend imports + runs it) ## 3. MemoryBackend - [x] 3.1 Implement `MemoryBackend` at `src/core/backend/memory/memory-backend.ts` (≤200 lines hard cap) - [x] 3.2 `read`, `readMulti`, `save`, `lastModified` — all four implemented - [x] 3.3 `seed` constructor option for tests - [x] 3.4 `memory-backend.test.ts` — seed read, save round-trip, lastModified monotonicity, readMulti merge - [x] 3.5 Sibling `.md` ## 4. HttpBackend - [x] 4.1 Implement `HttpBackend` at `src/core/backend/http/http-backend.ts` - [x] 4.2 `loadPath` template with `{locale}` / `{namespace}` placeholders - [x] 4.3 Injectable `fetchFn` (default `globalThis.fetch`), `parsePayload`, `customHeaders`, `crossDomain` - [x] 4.4 Map HTTP 404 → `BACKEND_LOCALE_NOT_AVAILABLE`; HTTP 5xx + `TypeError` (network) → `BACKEND_READ_FAILED` - [x] 4.5 Optional `retry: RetryPolicy` from `@suaveplan/retry` (peer; resolves automatically when present) - [x] 4.6 `HttpBackendOptionsSchema` (Zod) at `src/core/backend/http/http-backend.schema.ts` - [x] 4.7 `http-backend.test.ts` — templated URL, 404 mapping, 5xx mapping, parsePayload swap, custom headers callback - [x] 4.8 Sibling `.md` ## 5. StorageBackend - [x] 5.1 Add `@suaveplan/storage workspace:^` to `dependencies` in `packages/i18n/i18n/package.json` - [x] 5.2 Implement `StorageBackend` at `src/core/backend/storage/storage-backend.ts` - [x] 5.3 Three layouts: `single-key-per-namespace` (default), `single-key-per-locale`, `flat-keys` - [x] 5.4 Pluggable codec: `encode(record): Uint8Array` / `decode(bytes): TranslationRecord`; default JSON - [x] 5.5 `lastModified` via `AbstractStorage.getMetadata` when available - [x] 5.6 `StorageBackendOptionsSchema` (Zod) - [x] 5.7 `storage-backend.test.ts` — all three layouts round-trip, codec swap to MessagePack, missing key surfaces `BACKEND_LOCALE_NOT_AVAILABLE` - [x] 5.8 `storage-backend.spec-conformance.test.ts` — uses real `MemoryStorage` from `@suaveplan/storage`, asserts OTel `read.duration_ms` sample emission - [x] 5.9 Sibling `.md` covering all three layouts with tradeoffs ## 6. MultiFileFsBackend - [x] 6.1 Implement `MultiFileFsBackend` at `src/core/backend/multi-file-fs/multi-file-fs-backend.ts` - [x] 6.2 Three layouts: `nested` (default), `flat`, `glob` - [x] 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 - [x] 6.4 `watch: true` opt-in via `node:fs/promises.watch`; emit `loaded` event on file change - [x] 6.5 `MultiFileFsBackendOptionsSchema` (Zod) - [x] 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 - [x] 6.7 Sibling `.md` documenting the "many JSON files in many places" use case directly - [x] 6.8 Subpath export `./backend/multi-file-fs` in `package.json` so browser builds tree-shake the Node-only backend ## 7. ChainedBackend - [x] 7.1 Implement `ChainedBackend` at `src/core/backend/chained/chained-backend.ts` - [x] 7.2 `read` walks in order; first success wins; subsequent backends skipped - [x] 7.3 Cache write-through: after a downstream success, call `save` on every preceding backend that implements it - [x] 7.4 `readMulti` fans out concurrently, merges per `mergeStrategy: 'first-wins' | 'last-wins'` - [x] 7.5 `BACKEND_READ_FAILED` propagates without fallthrough; only `BACKEND_LOCALE_NOT_AVAILABLE` triggers fallthrough - [x] 7.6 `chained-backend.test.ts` — short-circuit on cache hit, fallthrough on locale-not-available, no fallthrough on read-failed, mergeStrategy semantics - [x] 7.7 `chained-backend.spec-conformance.test.ts` — write-through cache uses real `MemoryBackend` + stub `HttpBackend`; verifies exact call sequence - [x] 7.8 Sibling `.md` ## 8. Catalog integration - [x] 8.1 Extend `MessageCatalog` constructor to accept `backend?: Backend` or `backends?: Backend[]` - [x] 8.2 Implement `MessageCatalog.preload(locales, namespaces?) → Promise` with `readMulti` preference - [x] 8.3 Implement `MessageCatalog.invalidate(locale?, namespace?) → void` - [x] 8.4 Implement `MessageCatalog.getAsync(key, locale?) → Promise` with in-flight load dedup - [x] 8.5 Synchronous `get` / `t` MUST NOT trigger backend reads (verified by spec-conformance test) - [x] 8.6 Implement `saveMissing: true` option — best-effort write-back via `backend.save`; OTel `backend.write.total{outcome=error}` increment on failure (no throw) - [x] 8.7 Modify existing `MessageCatalog.load(locale, namespace?, loader?)` to make `loader` optional and route to backend when omitted - [x] 8.8 `catalog-backend.test.ts` — preload, invalidate, getAsync, saveMissing, sync-get-never-triggers-backend - [x] 8.9 Update `catalog.md` and root `README.md` `## Quick Start` with a `StorageBackend` example ## 9. OTel instrumentation - [x] 9.1 `i18n.backend.read.duration_ms` histogram emission wired in every concrete backend - [x] 9.2 `i18n.backend.read.total` and `i18n.backend.write.total` counters wired in every concrete backend - [x] 9.3 `i18n.backend.cache.size` observable gauge registered against `MessageCatalog`'s internal `Map.size` - [x] 9.4 OTel emission tested with real `InMemoryMetricExporter` in `storage-backend.spec-conformance.test.ts` and `catalog-backend.test.ts` ## 10. Wiring and exports - [x] 10.1 Update `src/index.ts` to export `Backend` interface, all five backend factories, and `MessageCatalog` async surface - [x] 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` - [x] 10.3 README `## Modules` table lists every new co-located `.md` - [x] 10.4 README `## Backends` section with one example per backend; the `MultiFileFsBackend` example explicitly demonstrates the `/.json` workflow ## 11. Pre-archive verification (the 5-gate triple) - [x] 11.1 `bun run check` — Biome 0/0/0 across the monorepo - [x] 11.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n` — green - [x] 11.3 `bunx turbo test:coverage --filter=@suaveplan/i18n` — 100/100/100/100 - [x] 11.4 `bun scripts/openspec-conformance.ts @suaveplan/i18n` — exit 0, no `error` findings - [x] 11.5 `bunx turbo build --filter=@suaveplan/i18n` — green ## 12. Pre-archive 7-row checklist (AGENTS.md Section 13) - [x] 12.1 Generics threaded; `expectTypeOf` assertions on `Backend.read` return type narrowing for typed message trees - [x] 12.2 Zod schemas wired at every public boundary (`HttpBackendOptionsSchema`, `StorageBackendOptionsSchema`, `MultiFileFsBackendOptionsSchema`, `ChainedBackendOptionsSchema`) - [x] 12.3 All four new error codes thrown from at least one production path - [x] 12.4 Strict-mode tested: `getAsync` rejection vs sync-get fallback paths - [x] 12.5 OTel histograms / counters / gauge emitted from production paths and asserted with a real in-memory `MeterProvider` - [x] 12.6 Lifecycle dedup: concurrent `preload(en, [auth])` calls trigger one backend read; concurrent `getAsync('auth:k')` calls trigger one backend read - [x] 12.7 No retroactive tick — every box reflects work done in this change ## 13. Archive - [x] 13.1 Fold spec delta into `openspec/specs/i18n/spec.md` - [x] 13.2 Move `openspec/changes/i18n-storage-backends/` → `openspec/archive/-i18n-storage-backends/` - [x] 13.3 Refresh `MANIFEST.md` if dep set changed (`@suaveplan/storage` is a new dep)