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

122 lines
8.6 KiB
Markdown

# 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<void>` with `readMulti` preference
- [x] 8.3 Implement `MessageCatalog.invalidate(locale?, namespace?) → void`
- [x] 8.4 Implement `MessageCatalog.getAsync(key, locale?) → Promise<LookupResult>` 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 `<locale>/<namespace>.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/<yyyy-mm-dd>-i18n-storage-backends/`
- [x] 13.3 Refresh `MANIFEST.md` if dep set changed (`@suaveplan/storage` is a new dep)