Initial Commit
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
# 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: `NumberFormatter` class — `format(value, locale, options?)` with `style: 'decimal' | 'percent' | 'currency'`, `currency`, `currencyDisplay`, `minimumFractionDigits`, `maximumFractionDigits`, `useGrouping`, `notation`, `signDisplay`. Validates options through a Zod schema; throws `I18nError` with code `I18N.INVALID_FORMATTER_OPTIONS` on rejection. Caches the underlying `Intl.NumberFormat` per (locale, options-hash) pair to amortise construction cost.
|
||||
- ADDED: `formatNumber(value, locale, options?)` standalone helper — pure-function wrapper around `NumberFormatter` for one-off calls.
|
||||
- ADDED: `RelativeTimeFormatter` class — `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 upstream `relative-time.ts` L40-L96.
|
||||
- ADDED: `formatRelativeTime(deltaMs, locale, options?)` standalone helper.
|
||||
- ADDED: `NumberFormatterOptions`, `NumberFormatterStyle`, `NumberFormatterCacheKey`, `RelativeTimeFormatterOptions`, `RelativeTimeUnit`, `RelativeTimeUnitSelection` types.
|
||||
- ADDED: `NumberFormatterOptionsSchema` and `RelativeTimeFormatterOptionsSchema` Zod schemas.
|
||||
- ADDED: `I18N.INVALID_FORMATTER_OPTIONS` and `I18N.UNSUPPORTED_LOCALE` error codes registered via `defineCodes`.
|
||||
- MODIFIED: `src/formatting/index.ts` — re-exports the two new modules alongside `currency` and `date`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **List formatting** (`Intl.ListFormat`) — separate change once `number` and `relative-time` ship 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.RelativeTimeFormat` already pluralises internally; manual integration with `@suaveplan/i18n` plural 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` — covers `Decimal/percent/currency formatting`, `Fraction-digit clamping`, `Cache reuse`
|
||||
- `packages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts` — pins Zod schema rejection, error-code emission, and `expectTypeOf` assertions on `NumberFormatterStyle`
|
||||
- `packages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts` — covers `Auto-unit selection`, `Style and numeric overrides`, `Negative deltas`
|
||||
- `packages/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"` → only `packages/i18n/i18n/package.json` itself).
|
||||
|
||||
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`.
|
||||
Reference in New Issue
Block a user