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

12 KiB

Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/primitives/i18n.

Tasks: enterprise-i18n

1. Scaffold

  • 1.1 Create packages/universal/i18n/ with package.json, tsconfig.json, tsconfig.lib.json, vite.config.ts, vitest.config.ts, README.md skeleton
  • 1.2 Register coverage thresholds at 100/100/100/100 in vitest.config.ts; provider = istanbul
  • 1.3 Add cross-cutting guard tests in tests/: dep-graph, no-disallowed, tree-shake
  • 1.4 Add package to root CLAUDE.md, README.md, and STRUCTURE.yml

2. Error types + Zod schemas

  • 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
  • 2.2 Implement src/error-codes.ts and auto-register via side-effect import in src/index.ts
  • 2.3 Implement src/schemas/catalog.schema.ts — TranslationRecordSchema (recursive), CatalogOptionsSchema
  • 2.4 Implement src/schemas/interpolation.schema.ts — InterpolationConfigSchema
  • 2.5 Implement src/schemas/resolver.schema.ts — ResolverConfigSchema
  • 2.6 Tests: error construction, code narrowing, schema parse/reject on valid and invalid inputs

3. Constants

  • 3.1 Implement src/constants/index.ts — DEFAULT_INTERPOLATION, DEFAULT_LOCALES, LOCALE_BCP47_MAP, LOCALE_DESCRIPTORS, RTL_LOCALES, DefaultLocale
  • 3.2 Ensure constants are tree-shakeable (no side effects)

4. Utils

  • 4.1 Implement src/utils/record.ts — flattenRecord(record, separator?), unflattenRecord(flat, separator?), countKeys(record), getNestedValue(record, key, separator?), hasKey(record, key, separator?)
  • 4.2 Implement src/utils/key.ts — buildKey(...segments), parseKey(key, separator?), KeyPathResult
  • 4.3 Implement src/utils/locale.ts — getTextDirection(locale), isRtl(locale), TextDirection
  • 4.4 Implement src/utils/sanitize.ts — sanitize(html, opts?), SanitizeOptions
  • 4.5 Unit tests for all util functions: round-trips, edge cases (empty strings, deeply nested, custom separator)
  • 4.6 Property-based tests: flattenRecord → unflattenRecord round-trip; buildKey / parseKey round-trip
  • 4.7 Co-located .md for each util file

5. Interpolation engine

  • 5.1 Implement src/interpolation/interpolation-engine.ts — InterpolationEngine class with .render(template, values, opts?) → InterpolationResult; config validation via InterpolationConfigSchema
  • 5.2 Implement src/interpolation/escape.ts — escapeHtmlChars(str), escapeRegex(str)
  • 5.3 Types: InterpolationConfig, InterpolationOptions, InterpolationResult, InterpolationValues, ValueFormatter
  • 5.4 escapeHtml option gates HTML escaping of interpolated values; custom ValueFormatter map for per-variable formatting
  • 5.5 Throws I18nError (code I18N.INTERPOLATION_ERROR) on unclosed variable delimiter or formatter error
  • 5.6 Unit tests: basic substitution, multiple variables, missing variable (fallback vs. error), formatter, HTML escaping, custom delimiters
  • 5.7 Property-based tests: interpolation with escapeHtml: true never emits unescaped <, >, &, ", '
  • 5.8 Co-located .md for each interpolation file

6. Pluralization engine

  • 6.1 Implement src/pluralization/plural-rules.ts — arabicPlural, chinesePlural, defaultPluralRule, englishPlural, frenchPlural, germanPlural, polishPlural, russianPlural, spanishPlural, welshPlural as PluralRuleFunction
  • 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
  • 6.3 Throws I18nError (code I18N.PLURAL_RULE_NOT_FOUND) only when strict mode is enabled and no rule exists
  • 6.4 Types: PluralCategory, PluralMessages, PluralResolveOptions, PluralResolveResult, PluralRuleFunction, PluralRuleRegistry
  • 6.5 Helpers: getBaseLocale(locale), normalizeLocale(locale)
  • 6.6 Unit tests: all language rules across representative counts; fallback chain; registry registration/overriding
  • 6.7 Property-based tests: normalizeLocale idempotent
  • 6.8 Co-located .md for each pluralization file

7. Message parser

  • 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
  • 7.2 Implement src/parser/render.ts — renderNodes(nodes, values, opts?) → string
  • 7.3 Implement src/parser/extract.ts — extractVariables(message) → string[]
  • 7.4 Node types: MessageNode, ParsedMessage, ParserOptions, PluralNode, SelectNode, TextNode, VariableNode
  • 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
  • 7.6 Co-located .md for each parser file

8. Message catalog

  • 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
  • 8.2 Implement src/catalog/deep-merge.ts — deepMerge(target, source) for TranslationRecord
  • 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
  • 8.4 Types: CatalogOptions, ExtendedCatalogOptions, LookupResult, MessageLoader, MissingKeyHandler, NamespacedCatalogMap, TranslationCatalogMap, TranslationRecord
  • 8.5 Unit tests: single-locale lookup, multi-namespace, lazy loading (real async), deep merge, missing-key handler, strict-mode throw, concurrent loader deduplication
  • 8.6 Co-located .md for each catalog file

9. Locale resolver

  • 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)
  • 9.2 Implement src/locale/normalize.ts — normalizeLocaleCode(code), extractLanguage(locale), extractRegion(locale); invalid locale throws I18nError (code I18N.INVALID_LOCALE)
  • 9.3 Types: ExtendedResolverConfig, ResolutionStrategy, ResolvedLocale, ResolverConfig
  • 9.4 Options validated via ResolverConfigSchema at construction
  • 9.5 Unit tests: each strategy source, fallback chain, BCP-47 normalization edge cases, invalid locale rejection
  • 9.6 Co-located .md for each locale resolver file

10. Locale middleware

  • 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
  • 10.2 Implement src/middleware/detect.ts — detectLocale(request, config) → Promise<ResolvedLocale>
  • 10.3 Implement src/middleware/direction.ts — getDirection(locale) → TextDirection
  • 10.4 Types: ExtendedMiddlewareConfig, I18nRequest, LocaleLookupSource, MiddlewareConfig, MiddlewareResult, TextDirection
  • 10.5 Unit tests: direction detection per locale, middleware attaches resolved locale, lookup source priority
  • 10.6 Co-located .md for each middleware file

11. Formatting: date

  • 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
  • 11.2 Implement src/formatting/date/date-range.ts — formatDateRange(start, end, locale, opts?) using Intl.DateTimeFormat.formatRange where available, otherwise two-part format
  • 11.3 Implement src/formatting/date/names.ts — getDayNames(locale, format?), getMonthNames(locale, format?) via Intl.DateTimeFormat
  • 11.4 Types: DateFormatOptions, DateFormatPreset
  • 11.5 Unit tests: all presets, custom options, day/month names for en, ar, zh, fr, de; range formatting
  • 11.6 Co-located .md for each date formatting file

12. Formatting: currency

  • 12.1 Implement src/formatting/currency/currency-format.ts — formatCurrency(amount, currency, locale, opts?), formatCurrencyValue(amount, currency, locale, opts?) backed by Intl.NumberFormat
  • 12.2 Implement src/formatting/currency/currency-info.ts — getCurrencyInfo(currency) → CurrencyInfo, getCurrencyName(currency, locale), getCurrencySymbol(currency, locale) via Intl.DisplayNames / Intl.NumberFormat
  • 12.3 Types: CommonCurrency (typed union of ISO 4217 codes), CurrencyFormatOptions, CurrencyInfo
  • 12.4 Unit tests: USD/EUR/GBP/JPY/CNY formatting per locale, symbol extraction, currency name localization
  • 12.5 Co-located .md for each currency formatting file

13. OTel instrumentation

  • 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)
  • 13.2 Wire i18nMetrics.translateTotal into MessageCatalog.get; wire i18nMetrics.localeResolveTotal into LocaleResolver.resolve; wire i18nMetrics.translateDuration histogram into MessageCatalog.get
  • 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

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

  • 15.1 Co-located .md for every implementation file (≥200 words, basic + advanced examples, API reference, implementation notes)
  • 15.2 Rewrite README.md with public API index, architecture diagram, consumer guide, and benchmark numbers
  • 15.3 bun run docs:check --filter=@suaveplan/i18n ≥ 90%

16. Verify and publish

  • 16.1 bunx turbo lint typecheck test --filter=@suaveplan/i18n green
  • 16.2 bunx turbo test:coverage --filter=@suaveplan/i18n at 100/100/100/100
  • 16.3 bunx turbo build --filter=@suaveplan/i18n green
  • 16.4 bun scripts/publish.ts --package @suaveplan/i18n to Verdaccio at 0.0.0
  • 16.5 Archive OpenSpec change to openspec/archive/2026-04-19-enterprise-i18n/