Files
2026-05-27 23:29:53 +01:00

6.1 KiB

Tasks: i18n-spec-conformance

1. Schemas + boundary validation

  • 1.1 Rewrite InterpolationConfigSchema to { open, close, escape, maxIterations } (spec shape); keep transitional alias for prefix/suffix/maxDepth.
  • 1.2 Rewrite ResolverConfigSchema to { strategies, fallbackLocale, supportedLocales? }.
  • 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).
  • 1.5 Add side-effect import ./error-codes.js to src/index.ts.

2. MessageCatalog conformance

  • 2.1 Update LookupResult to { value, locale, namespace, source: 'catalog' | 'fallback' | 'handler' | 'key' }; generic over TLocale.
  • 2.2 Add strict option; throw I18nError(I18N.MISSING_KEY) when on and no value found.
  • 2.3 Add set(locale, namespace, record); deprecate addMessages as alias.
  • 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).
  • 2.5 Update keys(locale, namespace?) to scope to namespace.
  • 2.6 Missing-key handler hit returns source: 'handler'.
  • 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

  • 3.1 Throw I18nError(I18N.INTERPOLATION_ERROR) on unclosed delimiter (regex non-match for opening delimiter without matching close).
  • 3.2 Wrap formatter exceptions as I18nError(I18N.INTERPOLATION_ERROR, ..., { cause }).
  • 3.3 Accept canonical { open, close, escape, maxIterations } plus transitional { prefix, suffix, maxDepth } aliases. (Schema accepts both shapes; engine reads open ?? prefix, close ?? suffix.)
  • 3.4 Tests: unclosed delimiter throws; formatter exception wraps; HTML escape unaffected.

4. PluralizationEngine

  • 4.1 Add strict?: boolean option; rule lookup chain: locale → base-locale → Intl.PluralRules → defaultPluralRule.
  • 4.2 In strict mode, after Intl.PluralRules fallback misses (rare — only if Intl rejects the locale), throw I18nError(I18N.PLURAL_RULE_NOT_FOUND).
  • 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

  • 5.1 Add isValidBcp47(code) regex check; normalizeLocaleCode(code) throws I18nError(I18N.INVALID_LOCALE) on garbage input.
  • 5.2 Tests: invalid input rejected; valid tags pass.

6. OTel instrumentation

  • 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.
  • 6.2 Add @opentelemetry/api to package deps via catalog:observability.
  • 6.3 Wire translateTotal + translateDuration into MessageCatalog.get.
  • 6.4 Wire localeResolveTotal into detectLocale (middleware).
  • 6.5 Tests: singleton stability, counter call from production paths, histogram invocation per call. (metrics.spec-conformance.test.ts.)

7. Typed key inference

  • 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).
  • 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

  • 8.1 Add import './error-codes.js'; at top of src/index.ts.
  • 8.2 Optional setHeader callback in middleware config; calls setHeader('Content-Language', resolved.locale) when provided.

9. Docs + structure

  • 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.
  • 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

  • 10.1 bunx turbo lint typecheck test --filter=@suaveplan/i18n green.
  • 10.2 bunx turbo test:coverage --filter=@suaveplan/i18n 100/100/100/100.
  • 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.