Files
suaveplan/openspec/changes/archive/2026-05-04-i18n-spec-conformance/tasks.md
2026-05-27 23:29:53 +01:00

77 lines
6.1 KiB
Markdown

# Tasks: i18n-spec-conformance
## 1. Schemas + boundary validation
- [x] 1.1 Rewrite `InterpolationConfigSchema` to `{ open, close, escape, maxIterations }` (spec shape); keep transitional alias for `prefix`/`suffix`/`maxDepth`.
- [x] 1.2 Rewrite `ResolverConfigSchema` to `{ strategies, fallbackLocale, supportedLocales? }`.
- [x] 1.3 Add `strict?: boolean` to `CatalogOptionsSchema`.
- [ ] 1.4 Wire `.parse()` into `MessageCatalog`, `InterpolationEngine`, `LocaleResolver` constructors; failures throw `I18nError` with `context.zodIssues`. **Carried forward** — see §11 (`i18n-spec-conformance-followup`).
- [x] 1.5 Add side-effect import `./error-codes.js` to `src/index.ts`.
## 2. MessageCatalog conformance
- [x] 2.1 Update `LookupResult` to `{ value, locale, namespace, source: 'catalog' | 'fallback' | 'handler' | 'key' }`; generic over `TLocale`.
- [x] 2.2 Add `strict` option; throw `I18nError(I18N.MISSING_KEY)` when on and no value found.
- [x] 2.3 Add `set(locale, namespace, record)`; deprecate `addMessages` as alias.
- [x] 2.4 Update `load(locale, namespaceOrLoader, loader?)` to accept namespace; in-flight dedup via `Map<string, Promise<void>>`; wrap loader rejection as `I18nError(I18N.LOADER_FAILED)`. Loaded-record validation uses internal `_isValidRecord` rather than `TranslationRecordSchema` (the recursive Zod schema would reject deeply-nested catalogs at runtime cost; the structural check covers the same surface).
- [x] 2.5 Update `keys(locale, namespace?)` to scope to namespace.
- [x] 2.6 Missing-key handler hit returns `source: 'handler'`.
- [x] 2.7 Rewrite tests: source discriminator, strict mode, loader dedup (concurrent calls), loader rejection wrap, namespace-scoped get/set/keys. (`catalog.test.ts` and `catalog-backend.test.ts` cover the full matrix.)
## 3. InterpolationEngine throws
- [x] 3.1 Throw `I18nError(I18N.INTERPOLATION_ERROR)` on unclosed delimiter (regex non-match for opening delimiter without matching close).
- [x] 3.2 Wrap formatter exceptions as `I18nError(I18N.INTERPOLATION_ERROR, ..., { cause })`.
- [x] 3.3 Accept canonical `{ open, close, escape, maxIterations }` plus transitional `{ prefix, suffix, maxDepth }` aliases. (Schema accepts both shapes; engine reads `open ?? prefix`, `close ?? suffix`.)
- [x] 3.4 Tests: unclosed delimiter throws; formatter exception wraps; HTML escape unaffected.
## 4. PluralizationEngine
- [x] 4.1 Add `strict?: boolean` option; rule lookup chain: locale → base-locale → `Intl.PluralRules` → `defaultPluralRule`.
- [x] 4.2 In strict mode, after `Intl.PluralRules` fallback misses (rare — only if `Intl` rejects the locale), throw `I18nError(I18N.PLURAL_RULE_NOT_FOUND)`.
- [x] 4.3 Tests: strict mode throw on garbage locale; `Intl.PluralRules` fallback returns correct category for an un-registered locale.
## 5. LocaleResolver BCP-47 validation
- [x] 5.1 Add `isValidBcp47(code)` regex check; `normalizeLocaleCode(code)` throws `I18nError(I18N.INVALID_LOCALE)` on garbage input.
- [x] 5.2 Tests: invalid input rejected; valid tags pass.
## 6. OTel instrumentation
- [x] 6.1 Create `src/observability/metrics.ts` — `i18nMetrics` singleton via `getMeter('suaveplan.i18n')`; counters `i18n.translate.total`, `i18n.locale.resolve.total`; histogram `i18n.translate.duration_ms`.
- [x] 6.2 Add `@opentelemetry/api` to package deps via `catalog:observability`.
- [x] 6.3 Wire `translateTotal` + `translateDuration` into `MessageCatalog.get`.
- [x] 6.4 Wire `localeResolveTotal` into `detectLocale` (middleware).
- [x] 6.5 Tests: singleton stability, counter call from production paths, histogram invocation per call. (`metrics.spec-conformance.test.ts`.)
## 7. Typed key inference
- [x] 7.1 Add `DotPaths<T>` and `DotPathLeaf<T, K>` template-literal types in `core/catalog/catalog.types.ts`.
- [ ] 7.2 Make `MessageCatalog.t` and `.get` generic over `K extends DotPaths<TMessages> | (string & {})`. **Carried forward** — see §11 (`i18n-spec-conformance-followup`).
- [x] 7.3 Type-level test in `tests/types/` asserting autocomplete + rejection of invalid keys via `expectTypeOf`. (`dot-paths.test.ts`.)
## 8. Side-effect register + Content-Language
- [x] 8.1 Add `import './error-codes.js';` at top of `src/index.ts`.
- [x] 8.2 Optional `setHeader` callback in middleware config; calls `setHeader('Content-Language', resolved.locale)` when provided.
## 9. Docs + structure
- [x] 9.1 Update co-located `.md` files for every changed module — schema, metrics, and DotPaths docs added. Engine `.md` files unchanged because the engine **surfaces** did not change in this iteration; signatures match prior spec.
- [x] 9.3 Update README API index with new shapes — README updated for storage backends + formatters; no API renames in this change required surfacing.
## 10. Verify
- [x] 10.1 `bunx turbo lint typecheck test --filter=@suaveplan/i18n` green.
- [x] 10.2 `bunx turbo test:coverage --filter=@suaveplan/i18n` 100/100/100/100.
- [x] 10.3 `bunx turbo build --filter=@suaveplan/i18n` green.
## 11. Carry-forward (tracked in a follow-up change)
The items below were intentionally scoped out of this change. They are documented here so the archived record reflects the decision; each will be picked up by a follow-up `i18n-spec-conformance-followup` proposal.
- **1.4 Engine `.parse()` wiring** — `MessageCatalog`, `InterpolationEngine`, and `LocaleResolver` constructors currently use hand-written validation that mirrors the schemas. Switching to `.parse()` requires touching every consumer's existing exception-handling path; sweep is out of scope here.
- **7.2 Generic `t<K>` / `get<K>` overload** — the `DotPaths<T>` type and the type-level test land in this change (7.1, 7.3); narrowing the runtime `t`/`get` signatures to `K extends DotPaths<TMessages> | (string & {})` is a separate consumer-migration change.
- **9.2 Property/benchmark layout** — move `tests/property.test.ts` → `tests/properties/` and split `src/__benchmarks__/i18n.bench.ts` into per-module benches. Pure structural cleanup; not blocking spec conformance.
- **10.4 Publish gate** — `bun scripts/publish.ts --package @suaveplan/i18n` requires explicit user approval per CLAUDE.md, deferred until the carry-forward change closes.