Initial Commit
This commit is contained in:
176
openspec/changes/archive/2026-05-04-i18n-completion/tasks.md
Normal file
176
openspec/changes/archive/2026-05-04-i18n-completion/tasks.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Tasks — i18n completion (ergonomic t, full ICU, plugin surface, Intl wrappers)
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick.
|
||||
|
||||
## Sequencing
|
||||
|
||||
This change SHOULD land **after** `i18n-storage-backends` (the `Backend` interface is referenced by `createMissingKeyReporter` and the `loaded` event topic). It can land before or after `i18n-formatters` — the `formatList` / `formatDisplayName` / number-helper additions here are independent of `formatNumber` / `formatRelativeTime`.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `openspec/specs/i18n/spec.md` end-to-end (canonical capability spec)
|
||||
- [x] 1.2 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.3 Confirm the in-flight `i18n-storage-backends` + `i18n-formatters` + `i18n-spec-conformance` proposals do not conflict with this delta (especially the `MessageCatalog` constructor surface and OTel meter naming)
|
||||
- [x] 1.4 `openspec change validate i18n-completion` exits with no errors
|
||||
|
||||
## 2. Ergonomic t() and TranslateOptions
|
||||
|
||||
- [x] 2.1 Define `TranslateOptions` and `PluralKey` in `packages/i18n/i18n/src/core/catalog/translate.types.ts`
|
||||
- [x] 2.2 Implement `MessageCatalog.t(key, options?)` accepting the options bag (≤300 lines after refactor)
|
||||
- [x] 2.3 Implement `count` → `PluralizationEngine.resolve` → branch selection with `count` auto-injected into `values`
|
||||
- [x] 2.4 Implement `context` → `${key}_${context}` lookup with fallback to base
|
||||
- [x] 2.5 Implement `defaultValue` fallback on miss
|
||||
- [x] 2.6 Implement nested-key references `$t(other.key)` / `$t(ns:key)` with depth limit (default 4) and cycle detection
|
||||
- [x] 2.7 Add `I18N.NESTING_DEPTH_EXCEEDED` error code via `defineCodes`
|
||||
- [x] 2.8 `t.plural(key, count, values?)` shortcut helper
|
||||
- [x] 2.9 `translate.test.ts` — count, context, defaultValue, nested refs (success + cycle), shortcut helper
|
||||
- [x] 2.10 Sibling `translate.md`
|
||||
|
||||
## 3. ICU MessageFormat — selectordinal
|
||||
|
||||
- [x] 3.1 Add `SelectOrdinalNode` to `MessageNode` discriminated union
|
||||
- [x] 3.2 Implement `parsePluralChoices(...)` branching for `selectordinal` (separate path so `plural` and `selectordinal` carry distinct `type` discriminants)
|
||||
- [x] 3.3 Render path calls `pluralSelect(count, 'ordinal')` (or, when wired through `MessageCatalog.t`, `PluralizationEngine.getCategory(count, locale, true)`)
|
||||
- [x] 3.4 `message.test.ts` — selectordinal selects `'few'` for English `3`, `'one'` for `1`, `'two'` for `2`
|
||||
|
||||
## 4. ICU MessageFormat — typed arguments
|
||||
|
||||
- [x] 4.1 Implement `parseTypedArg(message, start, name)` for `{name, date|time|number, style?}` shapes
|
||||
- [x] 4.2 Add `TypedArgumentNode` to `MessageNode` discriminated union with `type: 'date' | 'time' | 'number'` and `style: string`
|
||||
- [x] 4.3 Render path calls `formatDate` / `formatTime` / `formatNumber` from `@suaveplan/i18n/formatting/*`
|
||||
- [x] 4.4 Number style parsing: `decimal`, `percent`, `currency/<ISO>`, `compact`, `scientific`, `engineering`
|
||||
- [x] 4.5 Date/time style parsing: `short`, `medium`, `long`, `full`
|
||||
- [x] 4.6 `message.test.ts` — typed currency, typed date, typed percent, typed compact
|
||||
- [x] 4.7 `MessageParser.validate(message)` rejects unknown `type` values
|
||||
|
||||
## 5. ICU MessageFormat — apostrophe quoting
|
||||
|
||||
- [x] 5.1 Pre-process the source message in `parseIcu` to handle the three apostrophe rules
|
||||
- [x] 5.2 `''` → single literal `'`
|
||||
- [x] 5.3 `'<followed by`{`,`}`, or another`'`>...'` → quoted run, content is literal until matching `'`
|
||||
- [x] 5.4 Standalone `'` (any other context) → literal apostrophe
|
||||
- [x] 5.5 `message.test.ts` — `it''s here`, `'{literal}' is X`, `don't`
|
||||
|
||||
## 6. ICU MessageFormat — tag interpolation
|
||||
|
||||
- [x] 6.1 Add `TagNode { type: 'tag'; name: string; children: MessageNode[] }` to `MessageNode`
|
||||
- [x] 6.2 `parseIcu` recognises `<name>...</name>` pairs; nested tags supported
|
||||
- [x] 6.3 Implement `MessageParser.format(message, values)` where a value `name: (chunks: string) => string` renders the tag inline
|
||||
- [x] 6.4 Implement `MessageParser.formatChunks<T>(message, values, chunkValues)` returning `Array<string | T>` — the React-friendly path
|
||||
- [x] 6.5 `extractVariables` skips tag names (they're not interpolation variables)
|
||||
- [x] 6.6 `message.test.ts` — inline string tag, nested tags, formatChunks mixed array
|
||||
- [x] 6.7 Update `message.md` with the rich-text section and a React example referencing `@suaveplan/react-i18n`
|
||||
|
||||
## 7. MessageParser.validate
|
||||
|
||||
- [x] 7.1 Implement `validate(message): ValidationResult` returning `{ ok: true } | { ok: false; errors: ValidationError[] }`
|
||||
- [x] 7.2 Detect: unbalanced braces, unbalanced apostrophe quoted runs, missing `other` branch in `plural` / `select` / `selectordinal`, unknown typed-argument type, unknown plural categories
|
||||
- [x] 7.3 `message-validate.test.ts` — happy path + each error case
|
||||
|
||||
## 8. I18nInstance facade and plugin lifecycle
|
||||
|
||||
- [x] 8.1 Define `I18nPlugin`, `I18nInstance`, `PluginRegistry` in `src/core/plugin/plugin.types.ts`
|
||||
- [x] 8.2 Implement `createI18n(options) → I18nInstance` composing existing engines + plugin registry
|
||||
- [x] 8.3 `i18n.use(plugin)` — sync + async install; reject duplicate names with `I18N.PLUGIN_ALREADY_INSTALLED`
|
||||
- [x] 8.4 `i18n.dispose()` — reverse-order plugin disposal
|
||||
- [x] 8.5 `i18n.changeLanguage(locale)` — emits `languageChanged`
|
||||
- [x] 8.6 Add `I18N.PLUGIN_ALREADY_INSTALLED` error code via `defineCodes`
|
||||
- [x] 8.7 `plugin.test.ts` — install once, dispose order, duplicate rejection
|
||||
- [x] 8.8 Sibling `plugin.md` walks through the plugin contract with three example plugins
|
||||
|
||||
## 9. Event bus topics
|
||||
|
||||
- [x] 9.1 Wire `@suaveplan/events` instance as `i18n.events`
|
||||
- [x] 9.2 Emit `languageChanged` from `changeLanguage`
|
||||
- [x] 9.3 Emit `loaded` from `MessageCatalog.set` / `mergeMessages` and from backend reads
|
||||
- [x] 9.4 Emit `missingKey` from `MessageCatalog.get` BEFORE invoking `MissingKeyHandler`
|
||||
- [x] 9.5 Emit `keyAdded` from `saveMissing` and HMR paths
|
||||
- [x] 9.6 Emit `pluralRuleAdded` from `PluralizationEngine.registerRule`
|
||||
- [x] 9.7 `events.test.ts` — every topic emits with the documented payload shape
|
||||
|
||||
## 10. List + display-name + number-helper formatters
|
||||
|
||||
- [x] 10.1 Implement `formatList(values, locale, options?)` at `src/formatting/list/list.ts` with cache + Zod options + OTel histogram
|
||||
- [x] 10.2 Implement `formatDisplayName(code, locale, options)` at `src/formatting/display-name/display-name.ts`
|
||||
- [x] 10.3 Implement `formatPercent`, `formatCompact`, `formatUnit` at `src/formatting/number/number-helpers.ts` — pre-bound `formatNumber` calls
|
||||
- [x] 10.4 `list.test.ts`, `display-name.test.ts`, `number-helpers.test.ts` — happy paths + Intl rejection mapping
|
||||
- [x] 10.5 Sibling `.md` for each
|
||||
- [x] 10.6 Subpath exports `./formatting/list` and `./formatting/display-name` in `package.json`
|
||||
|
||||
## 11. Pluralization Intl-first refactor
|
||||
|
||||
- [x] 11.1 Refactor `PluralizationEngine.getRule` to prefer override → `Intl.PluralRules` → hand-written → default
|
||||
- [x] 11.2 Hand-written rules remain registered (they're now overrides for runtimes lacking `Intl.PluralRules`)
|
||||
- [x] 11.3 `pluralization.test.ts` — override wins, Intl used when no override, hand-written used when Intl missing (stub `Intl.PluralRules` throw)
|
||||
|
||||
## 12. Locale-aware first-day-of-week
|
||||
|
||||
- [x] 12.1 Refactor `getDayNames(locale, style?)` to use `new Intl.Locale(locale).weekInfo.firstDay` when available
|
||||
- [x] 12.2 Fall back to Monday-start when `weekInfo` is undefined or throws
|
||||
- [x] 12.3 `date.test.ts` — `en-US` → Sunday, `en-GB` → Monday, fallback path
|
||||
|
||||
## 13. Accept-Language script casing
|
||||
|
||||
- [x] 13.1 Refactor `parseAcceptLanguage` to preserve script title-case + region upper-case + language lower-case
|
||||
- [x] 13.2 `negotiation.test.ts` — `zh-Hant-TW` round-trips with original casing; `EN-US;q=0.9` parses to `en-US`
|
||||
|
||||
## 14. Pseudo-localization
|
||||
|
||||
- [x] 14.1 Implement `pseudoLocalize(text, options?)` at `src/post-processors/pseudo-localize.ts` with three modes
|
||||
- [x] 14.2 Implement `pseudoLocalizationPlugin(options?)` returning an `I18nPlugin` that registers as a post-processor
|
||||
- [x] 14.3 `pseudo-localize.test.ts` — accent, bracket, expand modes; plugin transforms `t()` output
|
||||
|
||||
## 15. Segmenter and collator wrappers
|
||||
|
||||
- [x] 15.1 Implement `formatSegment(text, locale, granularity?)` at `src/locale/segment/segment.ts`
|
||||
- [x] 15.2 Implement `compareLocale(a, b, locale, options?)` at `src/locale/collator/collator.ts` with cache
|
||||
- [x] 15.3 `segment.test.ts`, `collator.test.ts` — non-Latin scripts (Japanese word segmentation, Arabic case-insensitive sort)
|
||||
|
||||
## 16. OTel instrumentation wiring
|
||||
|
||||
- [x] 16.1 Wire `i18n.translate.total{outcome,locale}` increment in `MessageCatalog.get`
|
||||
- [x] 16.2 Wire `i18n.translate.duration_ms{locale}` histogram in `MessageCatalog.get`
|
||||
- [x] 16.3 Wire `i18n.locale.resolve.total{strategy,locale}` increment in `LocaleResolver.resolve` and `negotiateLocale`
|
||||
- [x] 16.4 Add `i18n.interpolation.duration_ms` histogram and emit from `InterpolationEngine.render` and `MessageParser.format`
|
||||
- [x] 16.5 Add `i18n.missing_key.total{locale,namespace}` counter; increment from `MessageCatalog.get` on miss before handler invocation
|
||||
- [x] 16.6 `metrics.spec-conformance.test.ts` — every instrument records a sample under in-memory `MeterProvider`; assert exact attributes
|
||||
|
||||
## 17. Missing-key reporter plugin
|
||||
|
||||
- [x] 17.1 Implement `createMissingKeyReporter(options?)` returning an `I18nPlugin`
|
||||
- [x] 17.2 Default behaviour: log `warn` via `@suaveplan/logger` (peer; no-op when absent)
|
||||
- [x] 17.3 `onMissing(payload)` callback option
|
||||
- [x] 17.4 `persistMissing: true` opt-in routing to `catalog.saveMissing` (requires `i18n-storage-backends` `Backend` with `save`)
|
||||
- [x] 17.5 `missing-key-reporter.test.ts` — warn-by-default, callback, persistMissing path
|
||||
|
||||
## 18. Wiring and exports
|
||||
|
||||
- [x] 18.1 Add `@suaveplan/events workspace:^` and `@suaveplan/logger workspace:^` (peer-optional for the reporter) to `package.json`
|
||||
- [x] 18.2 Update `src/index.ts` to export `createI18n`, `I18nInstance`, `I18nPlugin`, `pseudoLocalize`, `formatList`, `formatDisplayName`, `formatPercent`, `formatCompact`, `formatUnit`, `formatSegment`, `compareLocale`, `createMissingKeyReporter`, `pseudoLocalizationPlugin`
|
||||
- [x] 18.3 Add subpath exports for `./post-processors`, `./formatting/list`, `./formatting/display-name`, `./formatting/number/number-helpers`, `./locale/segment`, `./locale/collator`, `./plugin`
|
||||
- [x] 18.4 Update root `README.md` `## Quick Start` with an `I18nInstance` + plugin example
|
||||
- [x] 18.5 Update `## Modules` table with every new co-located `.md`
|
||||
|
||||
## 19. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [x] 19.1 `bun run check` — Biome 0/0/0 across the monorepo
|
||||
- [x] 19.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n` — green
|
||||
- [x] 19.3 `bunx turbo test:coverage --filter=@suaveplan/i18n` — 100/100/100/100
|
||||
- [x] 19.4 `bun scripts/openspec-conformance.ts @suaveplan/i18n` — exit 0
|
||||
- [x] 19.5 `bunx turbo build --filter=@suaveplan/i18n` — green
|
||||
|
||||
## 20. Pre-archive 7-row checklist (AGENTS.md Section 13)
|
||||
|
||||
- [x] 20.1 Generics threaded; `expectTypeOf` assertions on `TranslateOptions['count']` narrowing, `MessageNode` discriminant
|
||||
- [x] 20.2 Zod schemas wired at every public boundary (`TranslateOptionsSchema`, `ListFormatOptionsSchema`, `DisplayNameOptionsSchema`, `PseudoLocalizeOptionsSchema`)
|
||||
- [x] 20.3 New error codes (`NESTING_DEPTH_EXCEEDED`, `PLUGIN_ALREADY_INSTALLED`) thrown from at least one production path
|
||||
- [x] 20.4 Strict-mode tested: cycle in nested refs, plugin duplicate, ICU validate failure
|
||||
- [x] 20.5 OTel histograms / counters emitted from production paths and asserted with a real in-memory `MeterProvider`
|
||||
- [x] 20.6 Lifecycle dedup: `changeLanguage` to same locale is a no-op (no event); plugin install / dispose idempotent
|
||||
- [x] 20.7 No retroactive tick — every box reflects work done in this change
|
||||
|
||||
## 21. Archive
|
||||
|
||||
- [x] 21.1 Fold spec delta into `openspec/specs/i18n/spec.md`
|
||||
- [x] 21.2 Move `openspec/changes/i18n-completion/` → `openspec/archive/<yyyy-mm-dd>-i18n-completion/`
|
||||
- [x] 21.3 Refresh `MANIFEST.md` if dep set changed (`@suaveplan/events`, optional `@suaveplan/logger`)
|
||||
Reference in New Issue
Block a user