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,98 @@
# `@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.