7.8 KiB
@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:
RelativeTimeFormatterclass wrappingIntl.RelativeTimeFormat(packages/i18n/i18n/src/formatting/relative-time/relative-time.tsL157–L168)formatRelativeTimestandalone helper with auto-unit-selection over a 7-tier threshold table (relative-time.tsL122–L151, L21–L78)- Per-(locale, style, numeric) cache (
relative-time.tsL14, L109–L120) INVALID_FORMATTER_OPTIONSon Zod failure (relative-time.tsL80–L89)UNSUPPORTED_LOCALEonIntl.RelativeTimeFormatRangeError(relative-time.tsL91–L107)- OTel histogram
i18n.format.relative_time.duration_msand cache-hit counter viaobservability/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—RelativeTimeFormatterclass +formatRelativeTimestandalone +clearRelativeTimeFormatterCachetest-only helper. Verbatim port of monolithrelative-time.ts(L1–L173) with import rewires. - ADDED:
src/formatter.types.ts—RelativeTimeUnit,RelativeTimeStyle,RelativeTimeNumeric,RelativeTimeFormatterOptions,RelativeTimeUnitSelection. Verbatim port of monolithrelative-time.types.ts(L1–L26). - ADDED:
src/formatter.schemas.ts—RelativeTimeFormatterOptionsSchemaZod schema. Verbatim port of monolithrelative-time.schemas.ts(L1–L22). - ADDED:
src/helpers.ts—formatTimeAgo(formatRelativeTime(deltaMs, locale, { style: "short" })shortcut),formatTimeIn(positive-delta variant offormatTimeAgo), andselectRelativeUnit(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 histogrami18n.format.relative_time.duration_ms+ counteri18n.format.cache_hit{formatter="relative_time"}. - ADDED:
src/formatter.test.ts— verbatim port of monolithrelative-time.test.ts(115 lines). - ADDED:
src/formatter.spec-conformance.test.ts— verbatim port of monolithrelative-time.spec-conformance.test.ts(121 lines). - ADDED:
src/helpers.test.ts— coversformatTimeAgo,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—RelativeTimeFormatterclass +formatRelativeTime+ types + schema only./helpers—formatTimeAgo,formatTimeIn,selectRelativeUnitonly
Dependencies
@suaveplan/i18n-shared workspace:^—I18N_CODES,I18nError@suaveplan/types workspace:^@suaveplan/error workspace:^@opentelemetry/api catalog:observabilitytslib catalog:buildzod catalog:forms- No dep on any other formatter package, on
@suaveplan/i18ncore, or on any backend package.
Out of scope
- Removing
formatting/relative-time/frompackages/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/formatRangeToPartsfor relative-time intervals — not part ofIntl.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_LOCALEpackages/i18n/i18n-formatter-relative-time/src/formatter.spec-conformance.test.ts— verbatim port of monolith spec-conformance suitepackages/i18n/i18n-formatter-relative-time/src/helpers.test.ts—formatTimeAgoreturns"5 sec. ago"-style output;formatTimeInreturns"in 5 sec."-style output;selectRelativeUnitreturns the chosen unit + value pairpackages/i18n/i18n-formatter-relative-time/src/observability.test.ts— histogram namedi18n.format.relative_time.duration_ms; counteri18n.format.cache_hit{formatter="relative_time"}increments only on second call with same optionspackages/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-numberunderpackages/i18n/. - Indirect consumer in Wave 6:
@suaveplan/i18nslim core wiresformatRelativeTimeinto the defaultFormatBridgefor ICU{delta, relativeTime}placeholder rendering. MANIFEST.mdandCLAUDE.mdupdated post-merge.