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

71 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- [x] Wave 1: `i18n-types-extension`, `i18n-shared` archived
- [x] Wave 2: `i18n-icu-parser` archived
- [x] Wave 3: 8 formatter packages archived (collator, currency, date, display-name, list, number, relative-time, segmenter)
- [x] `@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).
- [x] 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/`.
- [x] 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.
- [x] 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/`.
- [x] 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/`.
- [x] 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/`.
- [x] 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/`.
- [x] 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)
- [x] `@suaveplan/i18n` slim core landed. 15 workspace deps wired (`i18n-shared`, `i18n-icu-parser`, 8 formatters, `i18n-detector`, 4 universal backends).
- [x] 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.
- [x] `src/parser/format-bridge-default/format-bridge-default.ts` authored — wires the formatter packages into a `FormatBridge` consumers can pass to `MessageParser.format(...)` / `renderNodes(...)`.
- [x] 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.
- [x] 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.
- [x] Subpath shims: `src/subpaths/{detector,parser,errors}.ts` forward to the new sibling packages; `package.json` `exports` map points at `dist/subpaths/*.js`.
- [x] 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.
- [x] Bundle: 592 KB total / 15 modules / `dist/index.js` 4.76 KB (was ~3.5 MB / ~120 modules pre-split).
- [x] 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.