6.3 KiB
i18n number and relative-time formatters
Package: @suaveplan/i18n
Status: PROPOSED
Type: feature + spec-conformance
Why
@suaveplan/i18n currently exports only the currency and date modules from src/formatting/index.ts (packages/i18n/i18n/src/formatting/index.ts L1-L2). The upstream package ships two additional formatters — NumberFormatter and RelativeTimeFormatter — that wrap Intl.NumberFormat / Intl.RelativeTimeFormat with consistent option handling, locale validation, and structured error reporting. Without them, downstream consumers (drive UI, network admin pages, suppression dashboards) re-implement the same Intl wiring with subtle differences in fraction-digit clamping, currency-display defaults, and unit selection. Both legacy modules already exist at upstream parity (packages/universal/i18n/src/formatting/number/number.ts 119 L; packages/universal/i18n/src/formatting/relative-time/relative-time.ts 130 L) and pass the genesis quality bar — porting them closes the parity gap before consumers fork.
What changes
@suaveplan/i18n
- ADDED:
NumberFormatterclass —format(value, locale, options?)withstyle: 'decimal' | 'percent' | 'currency',currency,currencyDisplay,minimumFractionDigits,maximumFractionDigits,useGrouping,notation,signDisplay. Validates options through a Zod schema; throwsI18nErrorwith codeI18N.INVALID_FORMATTER_OPTIONSon rejection. Caches the underlyingIntl.NumberFormatper (locale, options-hash) pair to amortise construction cost. - ADDED:
formatNumber(value, locale, options?)standalone helper — pure-function wrapper aroundNumberFormatterfor one-off calls. - ADDED:
RelativeTimeFormatterclass —format(deltaMs, locale, options?)with auto-unit selection (seconds → minutes → hours → days → weeks → months → years),numeric: 'always' | 'auto',style: 'long' | 'short' | 'narrow'. Auto-selects the largest unit whose absolute value ≥ 1; threshold table mirrors upstreamrelative-time.tsL40-L96. - ADDED:
formatRelativeTime(deltaMs, locale, options?)standalone helper. - ADDED:
NumberFormatterOptions,NumberFormatterStyle,NumberFormatterCacheKey,RelativeTimeFormatterOptions,RelativeTimeUnit,RelativeTimeUnitSelectiontypes. - ADDED:
NumberFormatterOptionsSchemaandRelativeTimeFormatterOptionsSchemaZod schemas. - ADDED:
I18N.INVALID_FORMATTER_OPTIONSandI18N.UNSUPPORTED_LOCALEerror codes registered viadefineCodes. - MODIFIED:
src/formatting/index.ts— re-exports the two new modules alongsidecurrencyanddate.
Out of scope
- List formatting (
Intl.ListFormat) — separate change oncenumberandrelative-timeship and prove the formatter-cache pattern. - Display-name formatting (
Intl.DisplayNames) — separate change; depends on locale-data plumbing not yet wired in genesis. - Plural-rule integration into
RelativeTimeFormatter—Intl.RelativeTimeFormatalready pluralises internally; manual integration with@suaveplan/i18nplural rules is unnecessary and adds risk of double-pluralisation.
Source mapping
This change derives from ../suaveplan/enterprise/i18n.md and the upstream package source.
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
NumberFormatter class |
packages/universal/i18n/src/formatting/number/number.ts |
L1-L119 | Verbatim port; thread I18nError through |
formatNumber helper |
packages/universal/i18n/src/formatting/number/number.ts |
L101-L119 | Verbatim |
NumberFormatterOptions, NumberFormatterStyle types |
packages/universal/i18n/src/formatting/number/number.types.ts |
L1-L17 | Verbatim |
RelativeTimeFormatter class |
packages/universal/i18n/src/formatting/relative-time/relative-time.ts |
L1-L130 | Verbatim port; thresholds at L40-L96 |
formatRelativeTime helper |
packages/universal/i18n/src/formatting/relative-time/relative-time.ts |
L110-L130 | Verbatim |
RelativeTimeFormatterOptions, RelativeTimeUnit types |
packages/universal/i18n/src/formatting/relative-time/relative-time.types.ts |
L1-L23 | Verbatim |
Requirements with no source line
| Requirement | Justification |
|---|---|
NumberFormatterOptionsSchema / RelativeTimeFormatterOptionsSchema Zod schemas |
AGENTS.md §13 row 2 ("Zod schemas wired at every declared boundary"). Upstream uses runtime typeof guards. |
I18N.INVALID_FORMATTER_OPTIONS / I18N.UNSUPPORTED_LOCALE error codes |
AGENTS.md §13 row 3 ("every error code thrown by at least one production path"). Upstream throws plain Error. |
Per-(locale, options) Intl.NumberFormat / Intl.RelativeTimeFormat cache |
Performance. Constructing Intl.* is ~50× slower than calling format(); without a cache the pattern goes quadratic on dashboards rendering hundreds of values. Not in upstream. |
Verification approach
Each requirement carries a **Verified by:** line in the spec delta.
packages/i18n/i18n/src/formatting/number/number.test.ts— coversDecimal/percent/currency formatting,Fraction-digit clamping,Cache reusepackages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts— pins Zod schema rejection, error-code emission, andexpectTypeOfassertions onNumberFormatterStylepackages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts— coversAuto-unit selection,Style and numeric overrides,Negative deltaspackages/i18n/i18n/src/formatting/relative-time/relative-time.spec-conformance.test.ts— pins Zod schema rejection, unsupported-locale fallback, and OTel histogram emission
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/i18n to enumerate. Genesis grep result:
- No dependents (verified by
grep -rl "@suaveplan/i18n" /home/ubuntu/web/genesis/packages/ --include="package.json"→ onlypackages/i18n/i18n/package.jsonitself).
Pure additive change — existing formatting/currency and formatting/date re-exports unchanged.
Migration impact
None — additive only. Callers that already import formatCurrency / formatDate from @suaveplan/i18n/formatting are unaffected. New consumers can import formatNumber / formatRelativeTime from the same root or from the per-feature subpaths @suaveplan/i18n/formatting/number/number and @suaveplan/i18n/formatting/relative-time/relative-time.