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`.
|
||||
@@ -0,0 +1,143 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: i18n (i18n-formatters)
|
||||
|
||||
**Package**: `@suaveplan/i18n`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Number formatting with style and currency support
|
||||
|
||||
The package SHALL expose a `NumberFormatter` class and a `formatNumber(value, locale, options?)` standalone helper that wrap `Intl.NumberFormat`. The formatter SHALL support `style: 'decimal' | 'percent' | 'currency'`, `currency` (ISO 4217 code), `currencyDisplay` (`'symbol' | 'code' | 'name' | 'narrowSymbol'`), `minimumFractionDigits`, `maximumFractionDigits`, `useGrouping`, `notation` (`'standard' | 'scientific' | 'engineering' | 'compact'`), and `signDisplay`. Options SHALL be validated through `NumberFormatterOptionsSchema` at construction.
|
||||
|
||||
#### Scenario: decimal style produces a grouped number
|
||||
|
||||
- **WHEN** `formatNumber(1234567.89, 'en-US', { style: 'decimal', maximumFractionDigits: 2 })` is called
|
||||
- **THEN** the result equals `"1,234,567.89"`
|
||||
|
||||
#### Scenario: currency style honours `currencyDisplay`
|
||||
|
||||
- **WHEN** `formatNumber(42, 'en-GB', { style: 'currency', currency: 'GBP', currencyDisplay: 'code' })` is called
|
||||
- **THEN** the result contains the substring `"GBP"` (not the `£` glyph)
|
||||
|
||||
#### Scenario: percent style multiplies by 100
|
||||
|
||||
- **WHEN** `formatNumber(0.255, 'en-US', { style: 'percent', maximumFractionDigits: 1 })` is called
|
||||
- **THEN** the result equals `"25.5%"`
|
||||
|
||||
**Verified by:** `packages/i18n/i18n/src/formatting/number/number.test.ts::"formats decimals with grouping in en-US"` AND `packages/i18n/i18n/src/formatting/number/number.test.ts::"formats currency with currencyDisplay code"` AND `packages/i18n/i18n/src/formatting/number/number.test.ts::"formats percent by multiplying by 100"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: NumberFormatter validates options via Zod
|
||||
|
||||
The package SHALL validate every `NumberFormatterOptions` object through `NumberFormatterOptionsSchema` (Zod). Unknown styles, negative fraction digits, fraction-digit inversions (`min > max`), and unsupported locale strings SHALL be rejected with `I18nError`.
|
||||
|
||||
#### Scenario: unknown style rejected
|
||||
|
||||
- **WHEN** `new NumberFormatter({ style: 'magic' } as never)` is constructed
|
||||
- **THEN** it throws `I18nError` with code `I18N.INVALID_FORMATTER_OPTIONS` and `context.field === "style"`
|
||||
|
||||
#### Scenario: unsupported locale surfaces structured error
|
||||
|
||||
- **WHEN** `formatNumber(1, 'xx-INVALID-LOCALE-1234', {})` is called
|
||||
- **THEN** it throws `I18nError` with code `I18N.UNSUPPORTED_LOCALE` (the underlying `Intl.NumberFormat` `RangeError` is mapped, not propagated)
|
||||
|
||||
#### Scenario: fraction-digit inversion rejected
|
||||
|
||||
- **WHEN** options carry `{ minimumFractionDigits: 4, maximumFractionDigits: 2 }`
|
||||
- **THEN** Zod rejection produces `I18N.INVALID_FORMATTER_OPTIONS` with `context.field === "fractionDigits"`
|
||||
|
||||
**Verified by:** `packages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts::"rejects unknown style with INVALID_FORMATTER_OPTIONS"` AND `packages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts::"maps Intl RangeError to UNSUPPORTED_LOCALE"` AND `packages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts::"rejects fraction-digit inversion"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: NumberFormatter caches `Intl.NumberFormat` instances
|
||||
|
||||
The package SHALL cache the underlying `Intl.NumberFormat` keyed by (locale, canonicalised options hash). Repeat calls with the same key SHALL reuse the cached instance and emit the OTel counter `i18n.format.cache_hit` with attribute `formatter="number"`.
|
||||
|
||||
#### Scenario: cache reused on identical key
|
||||
|
||||
- **WHEN** `formatNumber(1, 'en-US', { style: 'decimal' })` is called twice
|
||||
- **THEN** the second call increments `i18n.format.cache_hit` by exactly 1
|
||||
|
||||
#### Scenario: distinct options produce distinct cache entries
|
||||
|
||||
- **WHEN** the same locale is called with `{ style: 'decimal' }` then `{ style: 'percent' }`
|
||||
- **THEN** both calls miss the cache; `i18n.format.cache_hit` does not increment
|
||||
|
||||
**Verified by:** `packages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts::"reuses cached Intl.NumberFormat on identical key"` AND `packages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts::"does not reuse cache across distinct options"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Relative-time formatting with auto-unit selection
|
||||
|
||||
The package SHALL expose a `RelativeTimeFormatter` class and a `formatRelativeTime(deltaMs, locale, options?)` helper that wrap `Intl.RelativeTimeFormat`. When `options.unit` is omitted, the formatter SHALL auto-select the largest unit whose absolute value is ≥ 1 from the ordered set `seconds`, `minutes`, `hours`, `days`, `weeks`, `months`, `years`. Threshold boundaries match upstream `relative-time.ts` L40-L96 (60s, 60m, 24h, 7d, 4.345w, 12mo).
|
||||
|
||||
#### Scenario: 30 seconds selects "seconds" unit
|
||||
|
||||
- **WHEN** `formatRelativeTime(30_000, 'en-US')` is called
|
||||
- **THEN** the result contains `"second"` (e.g. `"in 30 seconds"`) AND not `"minute"`
|
||||
|
||||
#### Scenario: 90 seconds rolls over to "minutes" unit
|
||||
|
||||
- **WHEN** `formatRelativeTime(90_000, 'en-US')` is called
|
||||
- **THEN** the selected unit is `"minutes"` and the value is `2` (rounded)
|
||||
|
||||
#### Scenario: negative delta produces past-tense phrasing
|
||||
|
||||
- **WHEN** `formatRelativeTime(-3_600_000, 'en-US', { numeric: 'auto' })` is called
|
||||
- **THEN** the result reads `"1 hour ago"` (past), not `"in 1 hour"` (future)
|
||||
|
||||
#### Scenario: explicit unit override skips auto-selection
|
||||
|
||||
- **WHEN** `formatRelativeTime(90_000, 'en-US', { unit: 'seconds' })` is called
|
||||
- **THEN** the formatter renders the value in seconds (`"in 90 seconds"`), not minutes
|
||||
|
||||
**Verified by:** `packages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts::"auto-selects seconds for sub-minute deltas"` AND `packages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts::"rolls over to minutes at the 60s boundary"` AND `packages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts::"renders past-tense phrasing for negative deltas"` AND `packages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts::"honours explicit unit override"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RelativeTimeFormatter style and numeric overrides
|
||||
|
||||
The package SHALL accept `style: 'long' | 'short' | 'narrow'` (default `'long'`) and `numeric: 'always' | 'auto'` (default `'always'`) on `RelativeTimeFormatter`. Both fields SHALL be validated through `RelativeTimeFormatterOptionsSchema`. Unknown values SHALL throw `I18nError` with code `I18N.INVALID_FORMATTER_OPTIONS`.
|
||||
|
||||
#### Scenario: short style produces abbreviated unit
|
||||
|
||||
- **WHEN** `formatRelativeTime(86_400_000, 'en-US', { style: 'short' })` is called
|
||||
- **THEN** the result contains `"day"` or `"days"` rendered in short form (e.g. `"in 1 day"` vs the long-style `"in 1 day"`; the test asserts the `Intl.RelativeTimeFormat({ style: 'short' })` output exactly)
|
||||
|
||||
#### Scenario: numeric `'auto'` produces phrases like "yesterday"
|
||||
|
||||
- **WHEN** `formatRelativeTime(-86_400_000, 'en-US', { numeric: 'auto' })` is called
|
||||
- **THEN** the result equals `"yesterday"` (the platform-defined phrase for –1 day)
|
||||
|
||||
#### Scenario: invalid style rejected
|
||||
|
||||
- **WHEN** `new RelativeTimeFormatter({ style: 'rad' } as never)` is constructed
|
||||
- **THEN** it throws `I18nError` with code `I18N.INVALID_FORMATTER_OPTIONS` and `context.field === "style"`
|
||||
|
||||
**Verified by:** `packages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts::"renders short style"` AND `packages/i18n/i18n/src/formatting/relative-time/relative-time.test.ts::"renders 'yesterday' under numeric: auto"` AND `packages/i18n/i18n/src/formatting/relative-time/relative-time.spec-conformance.test.ts::"rejects unknown style with INVALID_FORMATTER_OPTIONS"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Both formatters emit OTel duration histograms
|
||||
|
||||
The package SHALL emit `i18n.format.number.duration_ms` and `i18n.format.relative_time.duration_ms` histograms recording the wall-clock duration of each `format()` call. Histograms SHALL be tagged with `locale` and `style` attributes.
|
||||
|
||||
#### Scenario: histogram observes a sample per format call
|
||||
|
||||
- **WHEN** `formatNumber(1, 'en-US', { style: 'decimal' })` is called once
|
||||
- **THEN** the in-memory `MeterProvider` records exactly one sample under `i18n.format.number.duration_ms` with attributes `{ locale: 'en-US', style: 'decimal' }`
|
||||
|
||||
#### Scenario: relative-time histogram tags include selected unit
|
||||
|
||||
- **WHEN** `formatRelativeTime(90_000, 'en-US')` is called
|
||||
- **THEN** the histogram sample carries attribute `unit: 'minutes'` (the auto-selected unit)
|
||||
|
||||
**Verified by:** `packages/i18n/i18n/src/formatting/number/number.spec-conformance.test.ts::"records a duration_ms sample per call"` AND `packages/i18n/i18n/src/formatting/relative-time/relative-time.spec-conformance.test.ts::"tags histogram with auto-selected unit"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
91
openspec/changes/archive/2026-05-04-i18n-formatters/tasks.md
Normal file
91
openspec/changes/archive/2026-05-04-i18n-formatters/tasks.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 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<RelativeTimeUnit>` 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/<yyyy-mm-dd>-i18n-formatters/`
|
||||
- [x] 10.3 Refresh `MANIFEST.md` if package set or deps changed
|
||||
Reference in New Issue
Block a user