8.6 KiB
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.mdend-to-end (canonical capability spec) - 1.2 Read
openspec/AGENTS.mdSections 13 and 14 - 1.3 Read
packages/storage/storage/src/abstract/abstract.tsto confirm the public surface this change consumes - 1.4 Confirm
i18n-formattersandi18n-spec-conformanceare landed (or land in parallel) before this change archives — the OTel meter wiring depends on them - 1.5
openspec change validate i18n-storage-backendsexits with no errors
2. Backend interface and error codes
- 2.1 Create
packages/i18n/i18n/src/core/backend/backend.types.tswithBackend,BackendInitContext,LayoutOption,BackendCapabilitiestypes - 2.2 Add
I18N.BACKEND_READ_FAILED,I18N.BACKEND_WRITE_FAILED,I18N.BACKEND_NOT_REGISTERED,I18N.BACKEND_LOCALE_NOT_AVAILABLEtosrc/error-codes.tsviadefineCodes - 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
MemoryBackendatsrc/core/backend/memory/memory-backend.ts(≤200 lines hard cap) - 3.2
read,readMulti,save,lastModified— all four implemented - 3.3
seedconstructor 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
HttpBackendatsrc/core/backend/http/http-backend.ts - 4.2
loadPathtemplate with{locale}/{namespace}placeholders - 4.3 Injectable
fetchFn(defaultglobalThis.fetch),parsePayload,customHeaders,crossDomain - 4.4 Map HTTP 404 →
BACKEND_LOCALE_NOT_AVAILABLE; HTTP 5xx +TypeError(network) →BACKEND_READ_FAILED - 4.5 Optional
retry: RetryPolicyfrom@suaveplan/retry(peer; resolves automatically when present) - 4.6
HttpBackendOptionsSchema(Zod) atsrc/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:^todependenciesinpackages/i18n/i18n/package.json - 5.2 Implement
StorageBackendatsrc/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
lastModifiedviaAbstractStorage.getMetadatawhen available - 5.6
StorageBackendOptionsSchema(Zod) - 5.7
storage-backend.test.ts— all three layouts round-trip, codec swap to MessagePack, missing key surfacesBACKEND_LOCALE_NOT_AVAILABLE - 5.8
storage-backend.spec-conformance.test.ts— uses realMemoryStoragefrom@suaveplan/storage, asserts OTelread.duration_mssample emission - 5.9 Sibling
.mdcovering all three layouts with tradeoffs
6. MultiFileFsBackend
- 6.1 Implement
MultiFileFsBackendatsrc/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 dynamicimport()and surface a clearBACKEND_READ_FAILEDif peer is missing for a given file extension - 6.4
watch: trueopt-in vianode:fs/promises.watch; emitloadedevent 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
.mddocumenting the "many JSON files in many places" use case directly - 6.8 Subpath export
./backend/multi-file-fsinpackage.jsonso browser builds tree-shake the Node-only backend
7. ChainedBackend
- 7.1 Implement
ChainedBackendatsrc/core/backend/chained/chained-backend.ts - 7.2
readwalks in order; first success wins; subsequent backends skipped - 7.3 Cache write-through: after a downstream success, call
saveon every preceding backend that implements it - 7.4
readMultifans out concurrently, merges permergeStrategy: 'first-wins' | 'last-wins' - 7.5
BACKEND_READ_FAILEDpropagates without fallthrough; onlyBACKEND_LOCALE_NOT_AVAILABLEtriggers 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 realMemoryBackend+ stubHttpBackend; verifies exact call sequence - 7.8 Sibling
.md
8. Catalog integration
- 8.1 Extend
MessageCatalogconstructor to acceptbackend?: Backendorbackends?: Backend[] - 8.2 Implement
MessageCatalog.preload(locales, namespaces?) → Promise<void>withreadMultipreference - 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/tMUST NOT trigger backend reads (verified by spec-conformance test) - 8.6 Implement
saveMissing: trueoption — best-effort write-back viabackend.save; OTelbackend.write.total{outcome=error}increment on failure (no throw) - 8.7 Modify existing
MessageCatalog.load(locale, namespace?, loader?)to makeloaderoptional 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.mdand rootREADME.md## Quick Startwith aStorageBackendexample
9. OTel instrumentation
- 9.1
i18n.backend.read.duration_mshistogram emission wired in every concrete backend - 9.2
i18n.backend.read.totalandi18n.backend.write.totalcounters wired in every concrete backend - 9.3
i18n.backend.cache.sizeobservable gauge registered againstMessageCatalog's internalMap.size - 9.4 OTel emission tested with real
InMemoryMetricExporterinstorage-backend.spec-conformance.test.tsandcatalog-backend.test.ts
10. Wiring and exports
- 10.1 Update
src/index.tsto exportBackendinterface, all five backend factories, andMessageCatalogasync 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
## Modulestable lists every new co-located.md - 10.4 README
## Backendssection with one example per backend; theMultiFileFsBackendexample explicitly demonstrates the<locale>/<namespace>.jsonworkflow
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, noerrorfindings - 11.5
bunx turbo build --filter=@suaveplan/i18n— green
12. Pre-archive 7-row checklist (AGENTS.md Section 13)
- 12.1 Generics threaded;
expectTypeOfassertions onBackend.readreturn 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:
getAsyncrejection 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; concurrentgetAsync('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.mdif dep set changed (@suaveplan/storageis a new dep)