Files
suaveplan/openspec/archive/2026-05-09-i18n-formatter-relative-time/proposal.md
2026-05-27 23:29:53 +01:00

99 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `@suaveplan/i18n-formatter-relative-time` — `Intl.RelativeTimeFormat` wrapper
**Package**: `@suaveplan/i18n-formatter-relative-time` (NEW)
**Status**: PROPOSED
**Type**: feature (new universal-tier package; verbatim port of `formatting/relative-time/`)
**Date**: 2026-05-05
**Wave**: 3 (depends on Wave 1 foundation; required by Wave 6 core slim)
**Design doc**: `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md` (especially §5 formatter package shape, §5b FormatBridge, §7.1)
**Path**: `packages/i18n/i18n-formatter-relative-time/`
**Tier**: universal
## Why
The 14-package i18n modular split lifts each `Intl.*` wrapper out of the monolith into its own sibling package so consumers can pull only the formatters they actually use. `@suaveplan/i18n-formatter-relative-time` is the second Wave-3 package, following the canonical formatter shape (per-(locale, options-hash) cache, Zod-validated options, OTel histogram + cache-hit counter, helper + class twin surface).
Today `formatting/relative-time/` ships:
- `RelativeTimeFormatter` class wrapping `Intl.RelativeTimeFormat` (`packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` L157–L168)
- `formatRelativeTime` standalone helper with auto-unit-selection over a 7-tier threshold table (`relative-time.ts` L122–L151, L21–L78)
- Per-(locale, style, numeric) cache (`relative-time.ts` L14, L109–L120)
- `INVALID_FORMATTER_OPTIONS` on Zod failure (`relative-time.ts` L80–L89)
- `UNSUPPORTED_LOCALE` on `Intl.RelativeTimeFormat` `RangeError` (`relative-time.ts` L91–L107)
- OTel histogram `i18n.format.relative_time.duration_ms` and cache-hit counter via `observability/format-metrics.ts`
The auto-unit-selection logic (`autoSelectUnit` at `relative-time.ts` L54–L78) is what makes this package non-trivial: it consults a 7-tier threshold table (`THRESHOLDS_MS`, L21–L28) to pick the human-friendly unit (e.g. `36 h` rolls over to `2 days`).
## What changes
### New package: `@suaveplan/i18n-formatter-relative-time`
- ADDED: `packages/i18n/i18n-formatter-relative-time/` package scaffold (`package.json`, `tsconfig.json`, `vite.config.ts`, `README.md`).
- ADDED: `src/formatter.ts` — `RelativeTimeFormatter` class + `formatRelativeTime` standalone + `clearRelativeTimeFormatterCache` test-only helper. Verbatim port of monolith `relative-time.ts` (L1–L173) with import rewires.
- ADDED: `src/formatter.types.ts` — `RelativeTimeUnit`, `RelativeTimeStyle`, `RelativeTimeNumeric`, `RelativeTimeFormatterOptions`, `RelativeTimeUnitSelection`. Verbatim port of monolith `relative-time.types.ts` (L1–L26).
- ADDED: `src/formatter.schemas.ts` — `RelativeTimeFormatterOptionsSchema` Zod schema. Verbatim port of monolith `relative-time.schemas.ts` (L1–L22).
- ADDED: `src/helpers.ts` — `formatTimeAgo` (`formatRelativeTime(deltaMs, locale, { style: "short" })` shortcut), `formatTimeIn` (positive-delta variant of `formatTimeAgo`), and `selectRelativeUnit` (exposes the auto-unit-selection logic standalone for callers who want the chosen unit only). New ergonomic surface introduced by the split (the monolith only ships the class + standalone).
- ADDED: `src/observability.ts` — local OTel histogram `i18n.format.relative_time.duration_ms` + counter `i18n.format.cache_hit{formatter="relative_time"}`.
- ADDED: `src/formatter.test.ts` — verbatim port of monolith `relative-time.test.ts` (115 lines).
- ADDED: `src/formatter.spec-conformance.test.ts` — verbatim port of monolith `relative-time.spec-conformance.test.ts` (121 lines).
- ADDED: `src/helpers.test.ts` — covers `formatTimeAgo`, `formatTimeIn`, `selectRelativeUnit`.
- ADDED: `src/index.ts` — explicit named re-exports.
- ADDED: `src/formatter.md`, `src/helpers.md`, `src/observability.md` — sibling docs.
### Subpath exports
- `.` — root barrel
- `./formatter` — `RelativeTimeFormatter` class + `formatRelativeTime` + types + schema only
- `./helpers` — `formatTimeAgo`, `formatTimeIn`, `selectRelativeUnit` only
### Dependencies
- `@suaveplan/i18n-shared workspace:^` — `I18N_CODES`, `I18nError`
- `@suaveplan/types workspace:^`
- `@suaveplan/error workspace:^`
- `@opentelemetry/api catalog:observability`
- `tslib catalog:build`
- `zod catalog:forms`
- **No** dep on any other formatter package, on `@suaveplan/i18n` core, or on any backend package.
## Out of scope
- **Removing `formatting/relative-time/` from `packages/i18n/i18n/src/`** — happens in Wave 6.
- **Custom threshold tables** (e.g. business-day-aware rollover) — follow-up; the monolith ships fixed thresholds and so does this port.
- **`formatRange` / `formatRangeToParts` for relative-time intervals** — not part of `Intl.RelativeTimeFormat`; out of scope.
## Source mapping
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `RelativeTimeFormatter` class | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L157–L168 | Verbatim port; rewire imports |
| `formatRelativeTime` standalone | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L122–L151 | Verbatim port |
| `clearRelativeTimeFormatterCache` | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L170–L173 | Verbatim port |
| Auto-unit-selection (`autoSelectUnit`, `THRESHOLDS_MS`) | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L21–L78 | Verbatim port |
| Per-(locale, style, numeric) cache | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L14, L109–L120 | Verbatim port |
| `INVALID_FORMATTER_OPTIONS` mapping | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L80–L89 | Verbatim port |
| `UNSUPPORTED_LOCALE` mapping | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L91–L107 | Verbatim port |
| Type definitions | `packages/i18n/i18n/src/formatting/relative-time/relative-time.types.ts` | L1–L26 | Verbatim port |
| Zod schema | `packages/i18n/i18n/src/formatting/relative-time/relative-time.schemas.ts` | L1–L22 | Verbatim port |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Helpers `formatTimeAgo`, `formatTimeIn`, `selectRelativeUnit` | New ergonomic surface introduced by the split (monolith only ships class + standalone). Symmetric with how the number formatter ships `formatPercent` / `formatCompact` / `formatUnit` helpers alongside the standalone. |
| Subpath exports `./formatter`, `./helpers` | Tree-shaking — slim-core `FormatBridge` wiring imports only the standalone `formatRelativeTime` |
## Verification approach
- `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts` — auto-unit-selection at every threshold boundary (60s, 60m, 24h, 7d, 30.4d, 365.25d); explicit-unit override; class twins helper; Zod failure → `INVALID_FORMATTER_OPTIONS`; bogus locale → `UNSUPPORTED_LOCALE`
- `packages/i18n/i18n-formatter-relative-time/src/formatter.spec-conformance.test.ts` — verbatim port of monolith spec-conformance suite
- `packages/i18n/i18n-formatter-relative-time/src/helpers.test.ts` — `formatTimeAgo` returns `"5 sec. ago"`-style output; `formatTimeIn` returns `"in 5 sec."`-style output; `selectRelativeUnit` returns the chosen unit + value pair
- `packages/i18n/i18n-formatter-relative-time/src/observability.test.ts` — histogram named `i18n.format.relative_time.duration_ms`; counter `i18n.format.cache_hit{formatter="relative_time"}` increments only on second call with same options
- `packages/i18n/i18n-formatter-relative-time/src/index.test.ts` — every documented public export resolves; subpaths resolve
## Impact
- New universal-tier package — joins `@suaveplan/i18n-shared`, `@suaveplan/i18n-icu-parser`, and `@suaveplan/i18n-formatter-number` under `packages/i18n/`.
- Indirect consumer in Wave 6: `@suaveplan/i18n` slim core wires `formatRelativeTime` into the default `FormatBridge` for ICU `{delta, relativeTime}` placeholder rendering.
- `MANIFEST.md` and `CLAUDE.md` updated post-merge.