6.1 KiB
6.1 KiB
Tasks: i18n-spec-conformance
1. Schemas + boundary validation
- 1.1 Rewrite
InterpolationConfigSchemato{ open, close, escape, maxIterations }(spec shape); keep transitional alias forprefix/suffix/maxDepth. - 1.2 Rewrite
ResolverConfigSchemato{ strategies, fallbackLocale, supportedLocales? }. - 1.3 Add
strict?: booleantoCatalogOptionsSchema. - 1.4 Wire
.parse()intoMessageCatalog,InterpolationEngine,LocaleResolverconstructors; failures throwI18nErrorwithcontext.zodIssues. Carried forward — see §11 (i18n-spec-conformance-followup). - 1.5 Add side-effect import
./error-codes.jstosrc/index.ts.
2. MessageCatalog conformance
- 2.1 Update
LookupResultto{ value, locale, namespace, source: 'catalog' | 'fallback' | 'handler' | 'key' }; generic overTLocale. - 2.2 Add
strictoption; throwI18nError(I18N.MISSING_KEY)when on and no value found. - 2.3 Add
set(locale, namespace, record); deprecateaddMessagesas alias. - 2.4 Update
load(locale, namespaceOrLoader, loader?)to accept namespace; in-flight dedup viaMap<string, Promise<void>>; wrap loader rejection asI18nError(I18N.LOADER_FAILED). Loaded-record validation uses internal_isValidRecordrather thanTranslationRecordSchema(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.tsandcatalog-backend.test.tscover 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 readsopen ?? prefix,close ?? suffix.) - 3.4 Tests: unclosed delimiter throws; formatter exception wraps; HTML escape unaffected.
4. PluralizationEngine
- 4.1 Add
strict?: booleanoption; rule lookup chain: locale → base-locale →Intl.PluralRules→defaultPluralRule. - 4.2 In strict mode, after
Intl.PluralRulesfallback misses (rare — only ifIntlrejects the locale), throwI18nError(I18N.PLURAL_RULE_NOT_FOUND). - 4.3 Tests: strict mode throw on garbage locale;
Intl.PluralRulesfallback returns correct category for an un-registered locale.
5. LocaleResolver BCP-47 validation
- 5.1 Add
isValidBcp47(code)regex check;normalizeLocaleCode(code)throwsI18nError(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—i18nMetricssingleton viagetMeter('suaveplan.i18n'); countersi18n.translate.total,i18n.locale.resolve.total; histogrami18n.translate.duration_ms. - 6.2 Add
@opentelemetry/apito package deps viacatalog:observability. - 6.3 Wire
translateTotal+translateDurationintoMessageCatalog.get. - 6.4 Wire
localeResolveTotalintodetectLocale(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>andDotPathLeaf<T, K>template-literal types incore/catalog/catalog.types.ts. - 7.2 Make
MessageCatalog.tand.getgeneric overK 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 viaexpectTypeOf. (dot-paths.test.ts.)
8. Side-effect register + Content-Language
- 8.1 Add
import './error-codes.js';at top ofsrc/index.ts. - 8.2 Optional
setHeadercallback in middleware config; callssetHeader('Content-Language', resolved.locale)when provided.
9. Docs + structure
- 9.1 Update co-located
.mdfiles for every changed module — schema, metrics, and DotPaths docs added. Engine.mdfiles 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/i18ngreen. - 10.2
bunx turbo test:coverage --filter=@suaveplan/i18n100/100/100/100. - 10.3
bunx turbo build --filter=@suaveplan/i18ngreen.
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, andLocaleResolverconstructors 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 — theDotPaths<T>type and the type-level test land in this change (7.1, 7.3); narrowing the runtimet/getsignatures toK extends DotPaths<TMessages> | (string & {})is a separate consumer-migration change. - 9.2 Property/benchmark layout — move
tests/property.test.ts→tests/properties/and splitsrc/__benchmarks__/i18n.bench.tsinto per-module benches. Pure structural cleanup; not blocking spec conformance. - 10.4 Publish gate —
bun scripts/publish.ts --package @suaveplan/i18nrequires explicit user approval per CLAUDE.md, deferred until the carry-forward change closes.