Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# Capability Spec Delta: i18n (spec-conformance)
**Package**: `@suaveplan/i18n`
**Status**: PROPOSED
---
## MODIFIED Requirements
### I18N-02 — Zod schemas at the public boundary
`InterpolationConfigSchema`, `ResolverConfigSchema`, and `CatalogOptionsSchema` MUST match the spec field names and MUST be invoked at construction time:
- `InterpolationConfigSchema` validates `{ open: string (≥1, ≠close), close: string (≥1), escape: string (length 0 or 1), maxIterations: int 1..100 }`.
- `ResolverConfigSchema` validates `{ strategies: ResolutionStrategy[] (non-empty), fallbackLocale: string (≥1), supportedLocales?: string[] }`.
- `CatalogOptionsSchema` adds `strict?: boolean` (default `false`).
`MessageCatalog`, `InterpolationEngine`, and `LocaleResolver` constructors MUST call `.parse(options)` and re-throw failures as `I18nError` with the relevant code (`I18N.INVALID_LOCALE` for resolver, `I18N.INTERPOLATION_ERROR` for interpolation, `I18N.MISSING_KEY` is reserved; for catalog use a generic `I18N.INVALID_OPTIONS` — see ADDED below) and `context.zodIssues` populated with the issue array.
**Verified by:** `src/schemas/i18n.schemas.test.ts::"applies defaults on empty object"` AND `src/schemas/i18n.schemas.test.ts::"accepts custom values"` AND `src/conformance.test.ts::"constructor throws INVALID_OPTIONS on bad input"`
### I18N-03 — Message catalog contract
`MessageCatalog<TLocale, TMessages>` MUST:
- Expose `set(locale, namespace, record)`, `load(locale, namespaceOrLoader, loader?)` (overloaded so legacy `load(locale, loader)` still compiles), `keys(locale, namespace?)`, `has(key, locale?)`, `get(key, locale?)`, `t(key, locale?)`.
- Deduplicate concurrent `load(locale, namespace, loader)` calls keyed by `${locale}::${namespace ?? ''}` — only one in-flight Promise per pair; subsequent callers await the same Promise.
- Wrap loader rejection as `I18nError(I18N.LOADER_FAILED, 'i18n.loader.failed', { cause, context: { locale, namespace } })`.
- Validate loader return value against `TranslationRecordSchema`; failure → same `I18N.LOADER_FAILED`.
- Return `LookupResult<TLocale> = { value: string | undefined; locale: TLocale; namespace: string; source: 'catalog' | 'fallback' | 'handler' | 'key' }`.
- In strict mode (default `false`), throw `I18nError(I18N.MISSING_KEY)` when no value found after handler fallback; otherwise return `{ value: undefined, ..., source: 'key' }` (and `t()` returns the key string).
- Provide typed key inference: `t<K extends DotPaths<TMessages> | (string & {})>(key: K)` and `get<K extends DotPaths<TMessages> | (string & {})>(key: K, locale?)`.
**Verified by:** `src/conformance.test.ts::"strict mode throws I18N.MISSING_KEY on miss"` AND `src/conformance.test.ts::"load() wraps loader rejection as I18N.LOADER_FAILED"` AND `src/core/catalog/catalog.test.ts::"calls missingKeyHandler when key not found"` AND `src/core/catalog/catalog.test.ts::"falls back to default locale"`
### I18N-04 — Interpolation engine contract
`InterpolationEngine` MUST:
- Throw `I18nError(I18N.INTERPOLATION_ERROR)` when an opening delimiter is found without a matching closing delimiter.
- Wrap any thrown value from a `ValueFormatter` as `I18nError(I18N.INTERPOLATION_ERROR, 'i18n.interpolation.formatter_failed', { cause, context: { variable } })`.
- Accept canonical option keys `{ open, close, escape, maxIterations }`. Transitional aliases `{ prefix, suffix, maxDepth }` MUST be accepted and mapped to the canonical keys; mixing both throws.
**Verified by:** `src/conformance.test.ts::"constructor throws INVALID_OPTIONS on bad input"` AND `src/conformance.test.ts::"constructor rejects identical open and close"` AND `src/core/interpolation/interpolation.test.ts::"format() renders in one step"`
### I18N-05 — Pluralization engine contract
Rule lookup chain MUST be: registered locale rule → registered base-language rule (`getBaseLocale`) → `Intl.PluralRules(locale).select(n)` mapped to `PluralCategory` → `defaultPluralRule`. In strict mode, if `Intl.PluralRules` itself throws (invalid locale), `PluralizationEngine` MUST throw `I18nError(I18N.PLURAL_RULE_NOT_FOUND)`.
**Verified by:** `src/core/pluralization/pluralization.test.ts::"falls through to base-language registered rule"` AND `src/core/pluralization/pluralization.test.ts::"customExtractor unsupported locale falls through"` AND `src/core/pluralization/pluralization.test.ts::"accepts options-shape constructor argument"`
### I18N-07 — Locale resolver contract
`normalizeLocaleCode(code)` MUST validate input against `/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/` and throw `I18nError(I18N.INVALID_LOCALE)` for non-matching strings (including empty).
`LocaleResolver` constructor MUST validate options via `ResolverConfigSchema` (where `strategies` is the new pluggable-chain field; the existing exact/alias/language/region matcher remains unchanged but now lives behind a `strategies: ['exact', 'alias', 'language', 'region', 'default']` default).
**Verified by:** `src/conformance.test.ts::"normalizeLocaleCode throws I18N.INVALID_LOCALE on garbage"` AND `src/locale/resolver/resolver.test.ts::"exact match"` AND `src/locale/resolver/resolver.test.ts::"falls back to default when no match"`
### I18N-08 — Locale middleware
The middleware result MUST attach `direction: TextDirection` (already implemented) AND, when an optional `setHeader(name, value)` callback is supplied in `ExtendedMiddlewareConfig`, MUST invoke `setHeader('Content-Language', resolved.locale)` after the lookup completes.
**Verified by:** `src/middleware/server/server.test.ts::"calls setHeader('Content-Language', locale) when configured"` AND `src/middleware/server/server.test.ts::"detects from cookie"` AND `src/middleware/server/server.test.ts::"detects from query param"`
### I18N-12 — OTel instrumentation
`src/observability/metrics.ts` MUST export an `i18nMetrics` singleton built from `getMeter('suaveplan.i18n')`:
- `translateTotal: Counter` — incremented inside `MessageCatalog.get` with attributes `{ outcome: 'hit' | 'miss', locale }`.
- `localeResolveTotal: Counter` — incremented inside `detectLocale` with attributes `{ strategy, locale }`.
- `translateDuration: Histogram` — records elapsed milliseconds for each `MessageCatalog.get` call with attribute `{ locale }`.
All instruments MUST be no-ops when no exporter is registered (default behaviour of `@opentelemetry/api`).
**Verified by:** `src/conformance.test.ts` (OTel instrument smoke tests) AND `tests/no-disallowed.test.ts::"found source files to check"`
## ADDED Requirements
### I18N-16 — `INVALID_OPTIONS` error code
A new code `I18N.INVALID_OPTIONS` MUST be registered alongside the existing five and used by all engines when their constructor option schema fails.
**Verified by:** `src/errors/i18n-error.test.ts::"accepts all five error codes"` AND `src/conformance.test.ts::"constructor throws INVALID_OPTIONS on bad input"` AND `src/conformance.test.ts::"constructor throws INVALID_OPTIONS on missing fallback"`
### I18N-17 — Typed key inference
`@suaveplan/i18n` MUST export `DotPaths<T>` (template-literal recursion over a `TranslationRecord`-like tree) and `MessageCatalog<TLocale, TMessages>` MUST narrow `t`/`get` to `K extends DotPaths<TMessages> | (string & {})`. The `(string & {})` branch is the explicit escape hatch that preserves runtime flexibility while still letting TypeScript autocomplete known keys.
**Verified by:** `src/conformance.test.ts` (DotPaths typed-inference smoke test)
### I18N-18 — `Intl.PluralRules` fallback
`PluralizationEngine` MUST consult `Intl.PluralRules(locale).select(n)` after the registered-rule chain misses but before falling back to `defaultPluralRule`. This ensures CLDR-tracked languages added in future ICU updates work without registering a hand-rolled rule.
**Verified by:** `src/core/pluralization/pluralization.test.ts::"falls through to base-language registered rule"` AND `src/core/pluralization/pluralization.test.ts::"customExtractor unsupported locale falls through"`