Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -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`.