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

142 lines
12 KiB
Markdown

> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/primitives/i18n.
# Tasks: enterprise-i18n
## 1. Scaffold
- [x] 1.1 Create `packages/universal/i18n/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
- [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
## 2. Error types + Zod schemas
- [x] 2.1 Implement `src/errors/i18n-error.ts` — `I18nError` base extends `SuaveplanError`; `defineCodes` for `I18N.MISSING_KEY`, `I18N.INVALID_LOCALE`, `I18N.LOADER_FAILED`, `I18N.INTERPOLATION_ERROR`, `I18N.PLURAL_RULE_NOT_FOUND`
- [x] 2.2 Implement `src/error-codes.ts` and auto-register via side-effect import in `src/index.ts`
- [x] 2.3 Implement `src/schemas/catalog.schema.ts` — `TranslationRecordSchema` (recursive), `CatalogOptionsSchema`
- [x] 2.4 Implement `src/schemas/interpolation.schema.ts` — `InterpolationConfigSchema`
- [x] 2.5 Implement `src/schemas/resolver.schema.ts` — `ResolverConfigSchema`
- [x] 2.6 Tests: error construction, code narrowing, schema parse/reject on valid and invalid inputs
## 3. Constants
- [x] 3.1 Implement `src/constants/index.ts` — `DEFAULT_INTERPOLATION`, `DEFAULT_LOCALES`, `LOCALE_BCP47_MAP`, `LOCALE_DESCRIPTORS`, `RTL_LOCALES`, `DefaultLocale`
- [x] 3.2 Ensure constants are tree-shakeable (no side effects)
## 4. Utils
- [x] 4.1 Implement `src/utils/record.ts` — `flattenRecord(record, separator?)`, `unflattenRecord(flat, separator?)`, `countKeys(record)`, `getNestedValue(record, key, separator?)`, `hasKey(record, key, separator?)`
- [x] 4.2 Implement `src/utils/key.ts` — `buildKey(...segments)`, `parseKey(key, separator?)`, `KeyPathResult`
- [x] 4.3 Implement `src/utils/locale.ts` — `getTextDirection(locale)`, `isRtl(locale)`, `TextDirection`
- [x] 4.4 Implement `src/utils/sanitize.ts` — `sanitize(html, opts?)`, `SanitizeOptions`
- [x] 4.5 Unit tests for all util functions: round-trips, edge cases (empty strings, deeply nested, custom separator)
- [x] 4.6 Property-based tests: `flattenRecord` → `unflattenRecord` round-trip; `buildKey` / `parseKey` round-trip
- [x] 4.7 Co-located `.md` for each util file
## 5. Interpolation engine
- [x] 5.1 Implement `src/interpolation/interpolation-engine.ts` — `InterpolationEngine` class with `.render(template, values, opts?)` → `InterpolationResult`; config validation via `InterpolationConfigSchema`
- [x] 5.2 Implement `src/interpolation/escape.ts` — `escapeHtmlChars(str)`, `escapeRegex(str)`
- [x] 5.3 Types: `InterpolationConfig`, `InterpolationOptions`, `InterpolationResult`, `InterpolationValues`, `ValueFormatter`
- [x] 5.4 `escapeHtml` option gates HTML escaping of interpolated values; custom `ValueFormatter` map for per-variable formatting
- [x] 5.5 Throws `I18nError` (code `I18N.INTERPOLATION_ERROR`) on unclosed variable delimiter or formatter error
- [x] 5.6 Unit tests: basic substitution, multiple variables, missing variable (fallback vs. error), formatter, HTML escaping, custom delimiters
- [x] 5.7 Property-based tests: interpolation with `escapeHtml: true` never emits unescaped `<`, `>`, `&`, `"`, `'`
- [x] 5.8 Co-located `.md` for each interpolation file
## 6. Pluralization engine
- [x] 6.1 Implement `src/pluralization/plural-rules.ts` — `arabicPlural`, `chinesePlural`, `defaultPluralRule`, `englishPlural`, `frenchPlural`, `germanPlural`, `polishPlural`, `russianPlural`, `spanishPlural`, `welshPlural` as `PluralRuleFunction`
- [x] 6.2 Implement `src/pluralization/pluralization-engine.ts` — `PluralizationEngine` class with `PluralRuleRegistry`; `.resolve(count, locale, messages, opts?)` → `PluralResolveResult`; auto-falls back to base language rule then `defaultPluralRule`
- [x] 6.3 Throws `I18nError` (code `I18N.PLURAL_RULE_NOT_FOUND`) only when strict mode is enabled and no rule exists
- [x] 6.4 Types: `PluralCategory`, `PluralMessages`, `PluralResolveOptions`, `PluralResolveResult`, `PluralRuleFunction`, `PluralRuleRegistry`
- [x] 6.5 Helpers: `getBaseLocale(locale)`, `normalizeLocale(locale)`
- [x] 6.6 Unit tests: all language rules across representative counts; fallback chain; registry registration/overriding
- [x] 6.7 Property-based tests: `normalizeLocale` idempotent
- [x] 6.8 Co-located `.md` for each pluralization file
## 7. Message parser
- [x] 7.1 Implement `src/parser/message-parser.ts` — `MessageParser` class with `.parse(message, opts?)` → `ParsedMessage`; parses text, `{variable}`, `{count, plural, one{…} other{…}}`, `{gender, select, male{…} female{…} other{…}}` forms
- [x] 7.2 Implement `src/parser/render.ts` — `renderNodes(nodes, values, opts?)` → `string`
- [x] 7.3 Implement `src/parser/extract.ts` — `extractVariables(message)` → `string[]`
- [x] 7.4 Node types: `MessageNode`, `ParsedMessage`, `ParserOptions`, `PluralNode`, `SelectNode`, `TextNode`, `VariableNode`
- [x] 7.5 Unit tests: plain text, variable nodes, plural nodes (one/other/few/many/zero), select nodes, nested nodes, unknown format falls through as text
- [x] 7.6 Co-located `.md` for each parser file
## 8. Message catalog
- [x] 8.1 Implement `src/catalog/catalog.ts` — `MessageCatalog` class with `.get(key, locale, opts?)`, `.set(locale, ns, record)`, `.load(locale, ns, loader)`, `.has(key, locale)`, `.keys(locale, ns?)`; lazy loader deduplication; `MissingKeyHandler` hook; namespace-scoped key lookup with dot-path resolution
- [x] 8.2 Implement `src/catalog/deep-merge.ts` — `deepMerge(target, source)` for `TranslationRecord`
- [x] 8.3 Loader errors surface as `I18nError` (code `I18N.LOADER_FAILED`); missing keys invoke `MissingKeyHandler` or throw `I18nError` (code `I18N.MISSING_KEY`) in strict mode
- [x] 8.4 Types: `CatalogOptions`, `ExtendedCatalogOptions`, `LookupResult`, `MessageLoader`, `MissingKeyHandler`, `NamespacedCatalogMap`, `TranslationCatalogMap`, `TranslationRecord`
- [x] 8.5 Unit tests: single-locale lookup, multi-namespace, lazy loading (real async), deep merge, missing-key handler, strict-mode throw, concurrent loader deduplication
- [x] 8.6 Co-located `.md` for each catalog file
## 9. Locale resolver
- [x] 9.1 Implement `src/locale/locale-resolver.ts` — `LocaleResolver` class with `.resolve(request, config)` → `Promise<ResolvedLocale>`; pluggable strategy chain (cookie → query-param → accept-language header → default)
- [x] 9.2 Implement `src/locale/normalize.ts` — `normalizeLocaleCode(code)`, `extractLanguage(locale)`, `extractRegion(locale)`; invalid locale throws `I18nError` (code `I18N.INVALID_LOCALE`)
- [x] 9.3 Types: `ExtendedResolverConfig`, `ResolutionStrategy`, `ResolvedLocale`, `ResolverConfig`
- [x] 9.4 Options validated via `ResolverConfigSchema` at construction
- [x] 9.5 Unit tests: each strategy source, fallback chain, BCP-47 normalization edge cases, invalid locale rejection
- [x] 9.6 Co-located `.md` for each locale resolver file
## 10. Locale middleware
- [x] 10.1 Implement `src/middleware/locale-middleware.ts` — `createLocaleMiddleware(config)` → middleware function that reads locale from request, attaches `ResolvedLocale` to `I18nRequest`, and sets response `Content-Language` header
- [x] 10.2 Implement `src/middleware/detect.ts` — `detectLocale(request, config)` → `Promise<ResolvedLocale>`
- [x] 10.3 Implement `src/middleware/direction.ts` — `getDirection(locale)` → `TextDirection`
- [x] 10.4 Types: `ExtendedMiddlewareConfig`, `I18nRequest`, `LocaleLookupSource`, `MiddlewareConfig`, `MiddlewareResult`, `TextDirection`
- [x] 10.5 Unit tests: direction detection per locale, middleware attaches resolved locale, lookup source priority
- [x] 10.6 Co-located `.md` for each middleware file
## 11. Formatting: date
- [x] 11.1 Implement `src/formatting/date/date-format.ts` — `formatDate(date, locale, opts?)`, `formatDateOnly(date, locale, opts?)`, `formatTime(date, locale, opts?)` backed by `Intl.DateTimeFormat`; preset-based (`DateFormatPreset`) and custom `Intl.DateTimeFormatOptions` support
- [x] 11.2 Implement `src/formatting/date/date-range.ts` — `formatDateRange(start, end, locale, opts?)` using `Intl.DateTimeFormat.formatRange` where available, otherwise two-part format
- [x] 11.3 Implement `src/formatting/date/names.ts` — `getDayNames(locale, format?)`, `getMonthNames(locale, format?)` via `Intl.DateTimeFormat`
- [x] 11.4 Types: `DateFormatOptions`, `DateFormatPreset`
- [x] 11.5 Unit tests: all presets, custom options, day/month names for en, ar, zh, fr, de; range formatting
- [x] 11.6 Co-located `.md` for each date formatting file
## 12. Formatting: currency
- [x] 12.1 Implement `src/formatting/currency/currency-format.ts` — `formatCurrency(amount, currency, locale, opts?)`, `formatCurrencyValue(amount, currency, locale, opts?)` backed by `Intl.NumberFormat`
- [x] 12.2 Implement `src/formatting/currency/currency-info.ts` — `getCurrencyInfo(currency)` → `CurrencyInfo`, `getCurrencyName(currency, locale)`, `getCurrencySymbol(currency, locale)` via `Intl.DisplayNames` / `Intl.NumberFormat`
- [x] 12.3 Types: `CommonCurrency` (typed union of ISO 4217 codes), `CurrencyFormatOptions`, `CurrencyInfo`
- [x] 12.4 Unit tests: USD/EUR/GBP/JPY/CNY formatting per locale, symbol extraction, currency name localization
- [x] 12.5 Co-located `.md` for each currency formatting file
## 13. OTel instrumentation
- [x] 13.1 Implement `src/observability/metrics.ts` — create meter via `@opentelemetry/api`; export `i18nMetrics` singleton with counter `i18n.translate.total` (labels: `outcome`, `locale`), counter `i18n.locale.resolve.total` (labels: `strategy`, `locale`), histogram `i18n.translate.duration_ms` (label: `locale`)
- [x] 13.2 Wire `i18nMetrics.translateTotal` into `MessageCatalog.get`; wire `i18nMetrics.localeResolveTotal` into `LocaleResolver.resolve`; wire `i18nMetrics.translateDuration` histogram into `MessageCatalog.get`
- [x] 13.3 Unit tests: no-op without exporter; counter increments with correct labels on hit/miss; histogram records non-negative duration
## 14. Property tests + benchmarks
- [x] 14.1 Property-based tests in `tests/properties/`:
- `flattenRecord` → `unflattenRecord` round-trip (fast-check arbitrary nested string record)
- `normalizeLocale` idempotent (fast-check arbitrary string)
- Interpolation escaping never leaks raw HTML (fast-check arbitrary string values with HTML chars)
- `buildKey` / `parseKey` round-trip (fast-check non-empty dot-segment string arrays)
- [x] 14.2 Benchmarks in `src/__benchmarks__/`:
- `catalog.bench.ts` — `MessageCatalog.get` 100 000 iterations on 1 000-key catalog across 3 namespaces
- `interpolation.bench.ts` — `InterpolationEngine.render` 10 000 iterations with 5-variable template
- `pluralization.bench.ts` — `PluralizationEngine.resolve` 10 000 iterations cycling through English, Polish, Arabic counts
## 15. Docs
- [x] 15.1 Co-located `.md` for every implementation file (≥200 words, basic + advanced examples, API reference, implementation notes)
- [x] 15.2 Rewrite `README.md` with public API index, architecture diagram, consumer guide, and benchmark numbers
- [x] 15.3 `bun run docs:check --filter=@suaveplan/i18n` ≥ 90%
## 16. Verify and publish
- [x] 16.1 `bunx turbo lint typecheck test --filter=@suaveplan/i18n` green
- [x] 16.2 `bunx turbo test:coverage --filter=@suaveplan/i18n` at 100/100/100/100
- [x] 16.3 `bunx turbo build --filter=@suaveplan/i18n` green
- [x] 16.4 `bun scripts/publish.ts --package @suaveplan/i18n` to Verdaccio at 0.0.0
- [x] 16.5 Archive OpenSpec change to `openspec/archive/2026-04-19-enterprise-i18n/`