12 KiB
12 KiB
i18n implementation runbook (Wave 4–6)
All six waves complete (2026-05-09). Waves 1–6 archived. The
@suaveplan/i18nmodular split is closed: 14 sibling packages plus the slim integration core. The slim core re-exports every pre-split public symbol; downstream consumers (@suaveplan/react-i18n,react-ui,react-forms) continue to resolve every import. Seeopenspec/archive/2026-05-09-i18n-core-slim/for the Wave 6 closing change.
Done
- Wave 1:
i18n-types-extension,i18n-sharedarchived - Wave 2:
i18n-icu-parserarchived - Wave 3: 8 formatter packages archived (collator, currency, date, display-name, list, number, relative-time, segmenter)
@suaveplan/typesBackendinterface expanded to 5-flag (canRead: trueliteral +canReadMulti/canSave/canTrackModification/canWatch); addedBackendInitContext,LayoutOption,BackendFactory<TOptions>, optionaldispose?(). Spec block inopenspec/specs/types/spec.mdre-folded;packages/core/types/tests/i18n-backend.test.tsupdated to match (16+ cases).- Wave 4:
@suaveplan/i18n-detectorarchived. Chain orchestrator +bcp47Negotiate+getTextDirection/isRtl+ 9 detectors +defaultDetectorChain+ per-detector OTel + chain-exhausted counter all underpackages/i18n/i18n-detector/. 196 tests, 100/100/100/100 coverage; archived asopenspec/archive/2026-05-09-i18n-detector/. - Wave 5a:
runBackendConformancelifted into@suaveplan/i18n-shared/testingsubpath; spec re-folded intoopenspec/specs/i18n-shared/spec.mdwith newBackend conformance harnessrequirement. 100/100/100/100 coverage on the harness self-tests. - Wave 5a:
@suaveplan/i18n-backend-memoryarchived. FullMemoryBackend+memoryBackend()factory + ZodMemoryBackendOptionsSchema+ per-backend OTel (i18n.backend.memory.*) + AbortSignal cancellation + 5-flag capability matrix. Test suite usesrunBackendConformancefrom@suaveplan/i18n-shared/testing. 54 tests, 100/100/100/100 coverage; archived asopenspec/archive/2026-05-09-i18n-backend-memory/. - Wave 5b:
@suaveplan/i18n-backend-storagearchived.StorageBackend+storageBackend()factory + exporteddefaultCodec+ ZodStorageBackendOptionsSchema+ per-backend OTel (i18n.backend.storage.*withlayoutattribute) + AbortSignal cancellation + 5-flag capability matrix; ports the three wire layouts (single-key-per-namespace,single-key-per-locale,flat-keys) verbatim from the monolith with imports rewired to@suaveplan/i18n-sharedand@suaveplan/types. Test suite usesrunBackendConformanceagainst all three layouts. 97 tests, 100/100/100/100 coverage; archived asopenspec/archive/2026-05-09-i18n-backend-storage/. - Wave 5c:
@suaveplan/i18n-backend-httparchived. Read-onlyHttpBackend+httpBackend()factory + exporteddefaultParsePayload/buildUrlhelpers + ZodHttpBackendOptionsSchema+ per-backend OTel (i18n.backend.http.read_duration_ms/read_total{outcome}/read_errors_total{code,status?}) + AbortSignal cancellation (pre-aborted →BACKEND_TIMEOUT; mid-flight AbortError /ABORT_ERR→BACKEND_TIMEOUT; signal threaded intoRequestInit) + 5-flag capability matrix (canRead: true, all writes/watches false). Error semantics: 404 →BACKEND_LOCALE_NOT_AVAILABLE(chained-backend fallthrough); 5xx / network / parse failure →BACKEND_READ_FAILED(chain-stopping). PluggableparsePayload/customHeaders/requestOptions/crossDomain/ structurally-compatibleretry?: HttpBackendRetry;defaultFetch()fallback throwsBACKEND_READ_FAILEDwhenglobalThis.fetchis unavailable. Test suite usesrunBackendConformancefrom@suaveplan/i18n-shared/testingplus 50+ impl-specific cases. 70 tests, 100/100/100/100 coverage; archived asopenspec/archive/2026-05-09-i18n-backend-http/. - Wave 5d:
@suaveplan/i18n-backend-chainedarchived. Universal-tierChainedBackendorchestrator +chainedBackend()factory (array shorthand or full options form) + ZodChainedBackendOptionsSchema(rejects emptybackendsarrays asINVALID_OPTIONS) + per-backend OTel (i18n.backend.chained.read_duration_ms{backend,locale,namespace,outcome}histogram,read_total{backend,outcome}counter — extended outcome sethit/miss/error/cache_hit/cache_miss/primarywithcache_hitfor index 0,cache_missfor later-index serve,primaryfor single-backend chains;read_errors_total{backend,code},write_duration_ms/write_total{backend,outcome},fallthrough_total{from_backend,to_backend}per-link counter) + AbortSignal cancellation onread/readMulti/save(pre-aborted →BACKEND_TIMEOUT; threaded into every child call when child accepts a third positional arg) + capabilities matrix derived from union of children + write-through cache after successful read from index N to indices 0..N-1 (best-effort, swallowed) +readMulticoncurrent fan-out +first-wins(default) /last-winsmerge with gap-fill +savefan-out →BACKEND_WRITE_FAILEDwithcausepreserved +lastModifiedfirst-defined-across-children +initforward /disposereverse-order + defensiveCHAINED_BACKEND_ALL_FAILEDruntime guard for empty-chain bypasses past the schema. Test suite usesrunBackendConformancefrom@suaveplan/i18n-shared/testingplus impl-specific cases. 78 tests, 100/100/100/100 coverage; archived asopenspec/archive/2026-05-09-i18n-backend-chained/. - Wave 5e:
@suaveplan/i18n-backend-fs-multifilearchived. Server-tierMultiFileFsBackend+multiFileFsBackend()factory + inlined ZodMultiFileFsBackendOptionsSchema+ per-backend OTel (i18n.backend.fs_multifile.read_duration_ms{backend,locale,namespace,outcome,layout},read_total{backend,outcome,layout},read_errors_total{backend,code,layout},write_duration_ms/write_total{backend,outcome,layout}) + AbortSignal cancellation onread/save(pre-aborted →BACKEND_TIMEOUT, no fs touch) + 5-flag capability matrix (canRead: true,canReadMulti: false,canSave: true,canTrackModification: true,canWatchfollowswatchoption). Three wire layouts (nested/flat/globwith{locale}/{namespace}substitution); JSON built-in encoder/decoder; YAML/YML and JSON5 auto-detected via dynamicimport(); missing peers surface asBACKEND_READ_FAILEDwithpeerin error context. fs.watch loop emitsloadedevents throughBackendInitContext.emitLoadedfor bothnestedandflatlayouts. ENOENT/ENOTDIR →BACKEND_LOCALE_NOT_AVAILABLE; other errno →BACKEND_READ_FAILED; writeFile failure →BACKEND_WRITE_FAILEDwithcausepreserved; unsupported encode extension →BACKEND_WRITE_FAILED.package.jsondeclaresengines.node >=20, omitsbrowserfield; leading// Server-tier packagecomment insrc/backend/backend.ts; NOT added to slim core universal-tier deps. Test suite usesrunBackendConformancefrom@suaveplan/i18n-shared/testingagainst a realnode:fs.mkdtemptmpdir (no fs mocks per CLAUDE.md "no mocks" rule) plus impl-specific cases over an injectedMultiFileFsLikeshim. 80 tests, 100/100/100/100 coverage; archived asopenspec/archive/2026-05-09-i18n-backend-fs-multifile/.
Wave 5 — Done
All five Wave 5 backends shipped: memory, storage, http, chained, fs-multifile.
Each backend package needs:
package.json(mirror@suaveplan/i18n-shared—sideEffects: false,exportsmap with./backend/./factory/./observabilitysubpaths each pointing at./dist/<feature>/index.{d.ts,js}, deps on@suaveplan/i18n-shared+@suaveplan/types+tslib+zod+@opentelemetry/api. Do NOT add a./schemassubpath — Zod schemas are inlined into the feature.tsand reachable via./backend)tsconfig.lib.jsonextending roottsconfig.base.jsonvite.config.ts(mirror i18n-shared with adjusted external + cacheDir;preserveModules: trueso the per-feature folder layout is mirrored underdist/)vitest.config.tswithprovider: 'istanbul', 100/100/100/100 thresholds- One feature per folder (CLAUDE.md "Package Structure" — file name MUST match folder name; canonical reference:
packages/i18n/i18n-detector/src/):src/backend/backend.ts—Backendclass. Port verbatim from monolith except:import { I18nError, I18N_CODES } from "@suaveplan/i18n-shared"andimport type { Backend, BackendCapabilities, BackendInitContext } from "@suaveplan/types". Inline the Zod boundary schema (<Name>BackendOptionsSchema) here — do NOT create a separatebackend.schemas.ts.src/backend/backend.types.ts— options + seed types (no.mdrequired for.types.ts).src/backend/backend.test.ts— runsrunBackendConformance(...)from@suaveplan/i18n-shared/testingplus impl-specific cases.src/backend/backend.md— sibling docs (≥200 words, basic + advanced, full API ref).src/backend/index.ts— feature barrel, explicit named re-exports only (CLAUDE.md §10 — NOexport *).src/factory/factory.{ts,test.ts,md}+src/factory/index.ts—<name>Backendfactory function.src/observability/observability.{ts,test.ts,md}+src/observability/index.ts— portrecordRead/recordWritefrompackages/i18n/i18n/src/observability/backend-metrics.ts, inline so leaf doesn't depend on monolith — emiti18n.backend.<name>.read_duration_ms,read_total{outcome},read_errors_total{code},write_duration_ms,write_total{outcome}.
src/index.ts— root barrel, explicit named re-exports from each feature folder (NOexport *).src/index.test.ts— covers root-barrel surface + every documented subpath resolves via./<feature>/index.js.README.mdwith Modules table linking to eachsrc/<feature>/<feature>.md.
After each backend ships:
- Tick
tasks.mdhonestly, foldspecs/<cap>/spec.md→openspec/specs/<cap>/spec.md, move folder toopenspec/archive/<date>-<cap>/ - Update
MANIFEST.mdviabun scripts/folder-reorg/generate-manifest.ts
Wave 6 — Done (2026-05-09)
@suaveplan/i18nslim core landed. 15 workspace deps wired (i18n-shared,i18n-icu-parser, 8 formatters,i18n-detector, 4 universal backends).- Root barrel
packages/i18n/i18n/src/index.tsrewritten per the back-compat re-export map. Every pre-split public symbol resolves through a sibling package. src/parser/format-bridge-default/format-bridge-default.tsauthored — wires the formatter packages into aFormatBridgeconsumers can pass toMessageParser.format(...)/renderNodes(...).- Catalog / plugin plumbing reviewed: the slim-core
MessageCatalog.tuses simple{var}replacement and never callsrenderNodesdirectly, so no internal bridge plumbing is required.defaultFormatBridgeis exported from the root barrel for consumer-driven ICU rendering. - Destructive deletes:
src/constants/,src/error-codes.ts,src/errors/,src/parser/message/,src/parser/index.ts,src/core/interpolation/,src/core/pluralization/,src/formatting/,src/detector/,src/core/backend/,src/locale/segment/,src/locale/collator/,src/observability/format-metrics.{ts,md}all removed. - Subpath shims:
src/subpaths/{detector,parser,errors}.tsforward to the new sibling packages;package.jsonexportsmap points atdist/subpaths/*.js. - 320 tests in the slim core pass; 100/100/100/100 coverage on the surviving modules. Downstream sweep (
bunx turbo lint typecheck test --filter='@suaveplan/react-*'): 40 tasks green (1238 tests inreact-uialone). Family sweep (@suaveplan/i18n*): 74 tasks green. - Bundle: 592 KB total / 15 modules /
dist/index.js4.76 KB (was ~3.5 MB / ~120 modules pre-split). - Archived as
openspec/archive/2026-05-09-i18n-core-slim/(with the cross-cutting design doc folded in).
The whole modular split is now complete (Waves 1–6).
Operational notes
- Turbo discovery now works in this WSL2 env (verified 2026-05-09 against
bunx turbo 2.9.6). Use the standard repo gates:bunx turbo lint typecheck test --filter=@suaveplan/<pkg>andbunx turbo test:coverage --filter=@suaveplan/<pkg>. The earlierbad pattern \\?\UNC\wsl.localhost\...failure is gone. - Heavy parallel work in other sessions on same monorepo causes additional contention. Coordinate or work in isolated worktree.
- Conformance harness lift is the sequencing keystone — do that first in Wave 5 before any backend, so each backend's test file has a clean import.