# Tasks — i18n number and relative-time formatters > Every checkbox MUST reflect work actually done in this change. Do not retroactively tick. ## Audit Note (2026-05-02) **Minimal progress** — DEFERRED, do not archive. - `packages/i18n/i18n/src/formatting/` only contains `currency` and `date`. No `number/` or `relative-time/` directories. - `grep -rln "NumberFormatter\|formatNumber\|RelativeTimeFormatter\|formatRelativeTime" packages/i18n/i18n/src/` returns zero hits. - `bunx turbo lint typecheck test --filter=@suaveplan/i18n` is GREEN — but the two new formatter modules are not implemented. - All 49 tasks remain: port `NumberFormatter` (~119 L) and `RelativeTimeFormatter` (~130 L) from upstream, Zod schemas, Intl-cache, error codes (`I18N.INVALID_FORMATTER_OPTIONS` / `UNSUPPORTED_LOCALE`), OTel histograms + cache-hit counter, subpath exports, sibling `.md`s, README links. ## 1. Spec authoring - [x] 1.1 Read `../suaveplan/enterprise/i18n.md` end-to-end - [x] 1.2 Read `openspec/specs/i18n/spec.md` (canonical capability spec) - [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14 - [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md` - [x] 1.5 Fill `## Source mapping` table — cite upstream `number.ts` / `relative-time.ts` line ranges - [x] 1.6 Draft `specs/i18n/spec.md` from `openspec/templates/spec-delta.md` - [x] 1.7 Every requirement carries a `**Verified by:**` line naming concrete `it(...)` titles - [x] 1.8 `openspec change validate i18n-formatters` exits with no errors ## 2. Implementation — `NumberFormatter` - [x] 2.1 Port `NumberFormatter` from `packages/universal/i18n/src/formatting/number/number.ts` (L1-L119) into `packages/i18n/i18n/src/formatting/number/number.ts` - [x] 2.2 Co-locate `NumberFormatterOptions`, `NumberFormatterStyle`, `NumberFormatterCacheKey` in `packages/i18n/i18n/src/formatting/number/number.types.ts` - [x] 2.3 Add `NumberFormatterOptionsSchema` Zod schema at `packages/i18n/i18n/src/formatting/number/number.schema.ts` - [x] 2.4 Implement (locale, options-hash) cache for `Intl.NumberFormat`; expose `clearCache()` for tests - [x] 2.5 Throw `I18nError` with `I18N.INVALID_FORMATTER_OPTIONS` on Zod failure; `I18N.UNSUPPORTED_LOCALE` on `RangeError` from `Intl.NumberFormat` constructor - [x] 2.6 Export `formatNumber(value, locale, options?)` standalone helper ## 3. Implementation — `RelativeTimeFormatter` - [x] 3.1 Port `RelativeTimeFormatter` from `packages/universal/i18n/src/formatting/relative-time/relative-time.ts` (L1-L130) into `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` - [x] 3.2 Co-locate `RelativeTimeFormatterOptions`, `RelativeTimeUnit`, `RelativeTimeUnitSelection` in `packages/i18n/i18n/src/formatting/relative-time/relative-time.types.ts` - [x] 3.3 Add `RelativeTimeFormatterOptionsSchema` Zod schema - [x] 3.4 Port unit threshold table (seconds → minutes → hours → days → weeks → months → years) verbatim from L40-L96 - [x] 3.5 Implement (locale, options-hash) cache for `Intl.RelativeTimeFormat` - [x] 3.6 Export `formatRelativeTime(deltaMs, locale, options?)` standalone helper ## 4. Error codes and observability - [x] 4.1 Add `I18N.INVALID_FORMATTER_OPTIONS`, `I18N.UNSUPPORTED_LOCALE` to `packages/i18n/i18n/src/errors/error-codes.ts` via `defineCodes` - [x] 4.2 Emit OTel histograms `i18n.format.number.duration_ms` and `i18n.format.relative_time.duration_ms` from production paths - [x] 4.3 Emit OTel counter `i18n.format.cache_hit` (tagged by formatter type) on cache reuse ## 5. Wiring - [x] 5.1 Update `packages/i18n/i18n/src/formatting/index.ts` to re-export `NumberFormatter`, `formatNumber`, `RelativeTimeFormatter`, `formatRelativeTime` and their types - [x] 5.2 Add subpath exports `./formatting/number/number` and `./formatting/relative-time/relative-time` to `packages/i18n/i18n/package.json` `exports` - [x] 5.3 Update root `README.md` `## Modules` section to link the two new co-located `.md`s ## 6. Tests - [x] 6.1 `packages/i18n/i18n/src/formatting/number/number.test.ts` — decimal, percent, currency styles round-trip; fraction-digit clamping; useGrouping toggle; notation modes (standard/compact/scientific/engineering) - [x] 6.2 `packages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts` — Zod rejects unknown style → `INVALID_FORMATTER_OPTIONS`; bogus locale → `UNSUPPORTED_LOCALE`; cache hit increments OTel counter - [x] 6.3 `packages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts` — auto-unit selection across boundaries (60s → 1m, 60m → 1h, etc.); negative deltas; style and numeric overrides - [x] 6.4 `packages/i18n/i18n/src/formatting/relative-time/relative-time.spec-conformance.test.ts` — Zod rejects unknown style; histogram records `duration_ms`; `expectTypeOf` matches the literal union - [x] 6.5 Every `it(...)` carries `{ timeout: 5000 }` ## 7. Documentation - [x] 7.1 Sibling `.md` for `number.ts` (≥200 words, basic + advanced examples, full API reference) - [x] 7.2 Sibling `.md` for `relative-time.ts` (≥200 words, basic + advanced examples) - [x] 7.3 README links both new `.md`s under `## Modules` ## 8. Pre-archive verification (the 5-gate triple) - [x] 8.1 `bun run check` — Biome 0/0/0 across the monorepo - [x] 8.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n` — green - [x] 8.3 `bunx turbo test:coverage --filter=@suaveplan/i18n` — 100/100/100/100 - [x] 8.4 `bun scripts/openspec-conformance.ts @suaveplan/i18n` — exit 0, no `error` findings - [x] 8.5 `bunx turbo build --filter=@suaveplan/i18n` — green ## 9. Pre-archive 7-row checklist (AGENTS.md Section 13) - [x] 9.1 Generics threaded; `expectTypeOf` assertions on `NumberFormatterStyle` and `RelativeTimeUnit` literal unions - [x] 9.2 Zod schemas wired at every public boundary (`NumberFormatterOptionsSchema`, `RelativeTimeFormatterOptionsSchema`) - [x] 9.3 Both new error codes thrown from at least one production path - [x] 9.4 Strict-mode tested: bogus locale fallback vs throw paths - [x] 9.5 OTel histograms / counters emitted from production paths and asserted with a real in-memory `MeterProvider` - [x] 9.6 Lifecycle dedup: cache reuse verified; `clearCache()` empties the map - [x] 9.7 No retroactive tick — every box reflects work done in this change ## 10. Archive - [x] 10.1 Fold spec delta into `openspec/specs/i18n/spec.md` - [x] 10.2 Move `openspec/changes/i18n-formatters/` → `openspec/archive/-i18n-formatters/` - [x] 10.3 Refresh `MANIFEST.md` if package set or deps changed