142 lines
12 KiB
Markdown
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/`
|