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

7.8 KiB
Raw Blame History

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