Files
suaveplan/openspec/archive/2026-05-12-i18n-implementation-runbook.md
2026-05-27 23:29:53 +01:00

12 KiB
Raw Permalink Blame History

i18n implementation runbook (Wave 4–6)

All six waves complete (2026-05-09). Waves 1–6 archived. The @suaveplan/i18n modular 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. See openspec/archive/2026-05-09-i18n-core-slim/ for the Wave 6 closing change.

Done

  • Wave 1: i18n-types-extension, i18n-shared archived
  • Wave 2: i18n-icu-parser archived
  • Wave 3: 8 formatter packages archived (collator, currency, date, display-name, list, number, relative-time, segmenter)
  • @suaveplan/types Backend interface expanded to 5-flag (canRead: true literal + canReadMulti/canSave/canTrackModification/canWatch); added BackendInitContext, LayoutOption, BackendFactory<TOptions>, optional dispose?(). Spec block in openspec/specs/types/spec.md re-folded; packages/core/types/tests/i18n-backend.test.ts updated to match (16+ cases).
  • Wave 4: @suaveplan/i18n-detector archived. Chain orchestrator + bcp47Negotiate + getTextDirection/isRtl + 9 detectors + defaultDetectorChain + per-detector OTel + chain-exhausted counter all under packages/i18n/i18n-detector/. 196 tests, 100/100/100/100 coverage; archived as openspec/archive/2026-05-09-i18n-detector/.
  • Wave 5a: runBackendConformance lifted into @suaveplan/i18n-shared/testing subpath; spec re-folded into openspec/specs/i18n-shared/spec.md with new Backend conformance harness requirement. 100/100/100/100 coverage on the harness self-tests.
  • Wave 5a: @suaveplan/i18n-backend-memory archived. Full MemoryBackend + memoryBackend() factory + Zod MemoryBackendOptionsSchema + per-backend OTel (i18n.backend.memory.*) + AbortSignal cancellation + 5-flag capability matrix. Test suite uses runBackendConformance from @suaveplan/i18n-shared/testing. 54 tests, 100/100/100/100 coverage; archived as openspec/archive/2026-05-09-i18n-backend-memory/.
  • Wave 5b: @suaveplan/i18n-backend-storage archived. StorageBackend + storageBackend() factory + exported defaultCodec + Zod StorageBackendOptionsSchema + per-backend OTel (i18n.backend.storage.* with layout attribute) + 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-shared and @suaveplan/types. Test suite uses runBackendConformance against all three layouts. 97 tests, 100/100/100/100 coverage; archived as openspec/archive/2026-05-09-i18n-backend-storage/.
  • Wave 5c: @suaveplan/i18n-backend-http archived. Read-only HttpBackend + httpBackend() factory + exported defaultParsePayload/buildUrl helpers + Zod HttpBackendOptionsSchema + 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 into RequestInit) + 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). Pluggable parsePayload / customHeaders / requestOptions / crossDomain / structurally-compatible retry?: HttpBackendRetry; defaultFetch() fallback throws BACKEND_READ_FAILED when globalThis.fetch is unavailable. Test suite uses runBackendConformance from @suaveplan/i18n-shared/testing plus 50+ impl-specific cases. 70 tests, 100/100/100/100 coverage; archived as openspec/archive/2026-05-09-i18n-backend-http/.
  • Wave 5d: @suaveplan/i18n-backend-chained archived. Universal-tier ChainedBackend orchestrator + chainedBackend() factory (array shorthand or full options form) + Zod ChainedBackendOptionsSchema (rejects empty backends arrays as INVALID_OPTIONS) + per-backend OTel (i18n.backend.chained.read_duration_ms{backend,locale,namespace,outcome} histogram, read_total{backend,outcome} counter — extended outcome set hit/miss/error/cache_hit/cache_miss/primary with cache_hit for index 0, cache_miss for later-index serve, primary for 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 on read/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) + readMulti concurrent fan-out + first-wins (default) / last-wins merge with gap-fill + save fan-out → BACKEND_WRITE_FAILED with cause preserved + lastModified first-defined-across-children + init forward / dispose reverse-order + defensive CHAINED_BACKEND_ALL_FAILED runtime guard for empty-chain bypasses past the schema. Test suite uses runBackendConformance from @suaveplan/i18n-shared/testing plus impl-specific cases. 78 tests, 100/100/100/100 coverage; archived as openspec/archive/2026-05-09-i18n-backend-chained/.
  • Wave 5e: @suaveplan/i18n-backend-fs-multifile archived. Server-tier MultiFileFsBackend + multiFileFsBackend() factory + inlined Zod MultiFileFsBackendOptionsSchema + 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 on read/save (pre-aborted → BACKEND_TIMEOUT, no fs touch) + 5-flag capability matrix (canRead: true, canReadMulti: false, canSave: true, canTrackModification: true, canWatch follows watch option). Three wire layouts (nested/flat/glob with {locale}/{namespace} substitution); JSON built-in encoder/decoder; YAML/YML and JSON5 auto-detected via dynamic import(); missing peers surface as BACKEND_READ_FAILED with peer in error context. fs.watch loop emits loaded events through BackendInitContext.emitLoaded for both nested and flat layouts. ENOENT/ENOTDIR → BACKEND_LOCALE_NOT_AVAILABLE; other errno → BACKEND_READ_FAILED; writeFile failure → BACKEND_WRITE_FAILED with cause preserved; unsupported encode extension → BACKEND_WRITE_FAILED. package.json declares engines.node >=20, omits browser field; leading // Server-tier package comment in src/backend/backend.ts; NOT added to slim core universal-tier deps. Test suite uses runBackendConformance from @suaveplan/i18n-shared/testing against a real node:fs.mkdtemp tmpdir (no fs mocks per CLAUDE.md "no mocks" rule) plus impl-specific cases over an injected MultiFileFsLike shim. 80 tests, 100/100/100/100 coverage; archived as openspec/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, exports map with ./backend/./factory/./observability subpaths each pointing at ./dist/<feature>/index.{d.ts,js}, deps on @suaveplan/i18n-shared + @suaveplan/types + tslib + zod + @opentelemetry/api. Do NOT add a ./schemas subpath — Zod schemas are inlined into the feature .ts and reachable via ./backend)
  • tsconfig.lib.json extending root tsconfig.base.json
  • vite.config.ts (mirror i18n-shared with adjusted external + cacheDir; preserveModules: true so the per-feature folder layout is mirrored under dist/)
  • vitest.config.ts with provider: '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 — Backend class. Port verbatim from monolith except: import { I18nError, I18N_CODES } from "@suaveplan/i18n-shared" and import type { Backend, BackendCapabilities, BackendInitContext } from "@suaveplan/types". Inline the Zod boundary schema (<Name>BackendOptionsSchema) here — do NOT create a separate backend.schemas.ts.
    • src/backend/backend.types.ts — options + seed types (no .md required for .types.ts).
    • src/backend/backend.test.ts — runs runBackendConformance(...) from @suaveplan/i18n-shared/testing plus 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 — NO export *).
    • src/factory/factory.{ts,test.ts,md} + src/factory/index.ts — <name>Backend factory function.
    • src/observability/observability.{ts,test.ts,md} + src/observability/index.ts — port recordRead / recordWrite from packages/i18n/i18n/src/observability/backend-metrics.ts, inline so leaf doesn't depend on monolith — emit i18n.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 (NO export *).
  • src/index.test.ts — covers root-barrel surface + every documented subpath resolves via ./<feature>/index.js.
  • README.md with Modules table linking to each src/<feature>/<feature>.md.

After each backend ships:

  • Tick tasks.md honestly, fold specs/<cap>/spec.md → openspec/specs/<cap>/spec.md, move folder to openspec/archive/<date>-<cap>/
  • Update MANIFEST.md via bun scripts/folder-reorg/generate-manifest.ts

Wave 6 — Done (2026-05-09)

  • @suaveplan/i18n slim 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.ts rewritten 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.ts authored — wires the formatter packages into a FormatBridge consumers can pass to MessageParser.format(...) / renderNodes(...).
  • Catalog / plugin plumbing reviewed: the slim-core MessageCatalog.t uses simple {var} replacement and never calls renderNodes directly, so no internal bridge plumbing is required. defaultFormatBridge is 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}.ts forward to the new sibling packages; package.json exports map points at dist/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 in react-ui alone). Family sweep (@suaveplan/i18n*): 74 tasks green.
  • Bundle: 592 KB total / 15 modules / dist/index.js 4.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> and bunx turbo test:coverage --filter=@suaveplan/<pkg>. The earlier bad 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.