71 lines
12 KiB
Markdown
71 lines
12 KiB
Markdown
# 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.
|