Files
2026-05-27 23:29:53 +01:00

9.1 KiB
Raw Permalink Blame History

@suaveplan/i18n-formatter-date — Intl.DateTimeFormat wrapper

Package: @suaveplan/i18n-formatter-date (NEW) Status: PROPOSED Type: feature (new universal-tier package; verbatim port of formatting/date/ plus class wrapper + Zod schema + observability uplift) 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) Path: packages/i18n/i18n-formatter-date/ Tier: universal

Why

The 14-package i18n modular split lifts each Intl.* wrapper out of the monolith into its own sibling package. @suaveplan/i18n-formatter-date ports the existing formatting/date/ module — the broadest of the formatters today — and brings it up to the canonical Wave-3 shape (Zod-validated options, OTel histogram + cache-hit counter, class wrapper for symmetry, sibling test files).

Today formatting/date/ ships a rich surface (180 LoC):

  • formatDate(date, locale, options?) standalone with preset-or-options resolution (packages/i18n/i18n/src/formatting/date/date.ts L64–L73)
  • formatTime(date, locale, long?) standalone (date.ts L79–L83) — short / long preset
  • formatDateOnly(date, locale, long?) standalone (date.ts L89–L97)
  • formatDateRange(start, end, locale, options?) standalone using formatRange (date.ts L104–L116)
  • getDayNames(locale, style?) ECMA-402-§10.6-getWeekInfo()-aware day-name list (date.ts L126–L160)
  • getMonthNames(locale, style?) (date.ts L166–L180)
  • 8-preset table (PRESET_MAP at L4–L41): short, medium, long, full, time-short, time-long, date-short, date-long
  • BCP-47 normalisation via LOCALE_BCP47_MAP from shared constants (date.ts L43–L49)
  • Type definitions (date.types.ts L1–L24): DateFormatPreset, DateFormatOptions, ExtendedDateOptions

This package introduces no new functionality beyond what the monolith ships — the changes are: (a) wrapping the standalones in a DateFormatter class for symmetry with NumberFormatter/RelativeTimeFormatter/ListFormatter, (b) adding Zod-validated options, (c) adding the canonical OTel histogram + cache-hit counter (the monolith's date formatter currently does not emit either), and (d) introducing a per-(locale, options-hash) cache (the monolith currently constructs a fresh Intl.DateTimeFormat per call).

What changes

New package: @suaveplan/i18n-formatter-date

  • ADDED: packages/i18n/i18n-formatter-date/ package scaffold (package.json, tsconfig.json, vite.config.ts, README.md).
  • ADDED: src/formatter.ts — DateFormatter class (NEW); formatDate standalone (port of monolith L64–L73) extended with cache and OTel emission; formatDateRange standalone (port of L104–L116); clearDateFormatterCache test-only helper. PRESET_MAP and resolvePreset helper (port of L4–L58). toBcp47 helper (port of L43–L49).
  • ADDED: src/formatter.types.ts — DateFormatPreset, DateFormatOptions, ExtendedDateOptions, DateFormatterCacheKey. Sourced from monolith date.types.ts L1–L24 plus new types for the class surface.
  • ADDED: src/formatter.schemas.ts — DateFormatterOptionsSchema Zod schema. NEW (the monolith does not currently Zod-validate options).
  • ADDED: src/helpers.ts — formatTime(date, locale, long?) (port of monolith L79–L83); formatDateOnly(date, locale, long?) (port of L89–L97); getDayNames(locale, style?) (port of L126–L160); getMonthNames(locale, style?) (port of L166–L180).
  • ADDED: src/observability.ts — local OTel histogram i18n.format.date.duration_ms + counter i18n.format.cache_hit{formatter="date"}.
  • ADDED: src/formatter.test.ts — port of monolith date.test.ts (185 lines) plus new tests for DateFormatter class, Zod-validation, cache reuse.
  • ADDED: src/formatter.spec-conformance.test.ts — every requirement scenario in this delta.
  • ADDED: src/helpers.test.ts — covers formatTime, formatDateOnly, getDayNames, getMonthNames (including getWeekInfo()-fallback for older runtimes).
  • 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 — DateFormatter class + formatDate + formatDateRange + types + schema only
  • ./helpers — formatTime, formatDateOnly, getDayNames, getMonthNames only

Dependencies

  • @suaveplan/i18n-shared workspace:^ — I18N_CODES, I18nError, LOCALE_BCP47_MAP
  • @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/date/ from packages/i18n/i18n/src/ — happens in Wave 6.
  • formatToParts for date output — out of scope for Wave 3; can be added as a follow-up if downstream consumers (e.g. react-data-table date columns) need part-by-part rendering.
  • Temporal API support — out of scope; Intl.DateTimeFormat accepts Date | number today and the monolith's surface matches that. Temporal-aware overloads are a follow-up once Temporal stabilises across runtimes.
  • Custom calendar systems beyond what Intl.DateTimeFormat supports — out of scope.

Source mapping

Requirement Source file Source lines Notes
formatDate standalone with preset-or-options resolution packages/i18n/i18n/src/formatting/date/date.ts L64–L73 Verbatim port; extended with cache + OTel emission
PRESET_MAP (8 presets: short/medium/long/full/time-short/time-long/date-short/date-long) packages/i18n/i18n/src/formatting/date/date.ts L4–L41 Verbatim port
formatTime standalone packages/i18n/i18n/src/formatting/date/date.ts L79–L83 Verbatim port (helper)
formatDateOnly standalone packages/i18n/i18n/src/formatting/date/date.ts L89–L97 Verbatim port (helper)
formatDateRange standalone packages/i18n/i18n/src/formatting/date/date.ts L104–L116 Verbatim port
getDayNames (getWeekInfo()-aware) packages/i18n/i18n/src/formatting/date/date.ts L126–L160 Verbatim port
getMonthNames packages/i18n/i18n/src/formatting/date/date.ts L166–L180 Verbatim port
toBcp47 BCP-47 normalisation packages/i18n/i18n/src/formatting/date/date.ts L43–L49 Verbatim port; use LOCALE_BCP47_MAP from @suaveplan/i18n-shared
resolvePreset preset+options merge packages/i18n/i18n/src/formatting/date/date.ts L51–L58 Verbatim port
DateFormatPreset, DateFormatOptions, ExtendedDateOptions types packages/i18n/i18n/src/formatting/date/date.types.ts L1–L24 Verbatim port

Requirements with no source line

Requirement Justification
DateFormatter class wrapper New — symmetric with NumberFormatter, RelativeTimeFormatter, ListFormatter, DisplayNameFormatter
DateFormatterOptionsSchema Zod schema New — current monolith does not validate options
INVALID_FORMATTER_OPTIONS mapping Bound to the new Zod schema
Per-(locale, options-hash) cache New — current monolith constructs a fresh Intl.DateTimeFormat per call; Wave-3 formatter shape mandates the cache for parity with sibling packages
OTel histogram i18n.format.date.duration_ms + cache-hit counter New — canonical Wave-3 formatter shape mandates these; monolith date formatter currently does not emit either

Verification approach

  • packages/i18n/i18n-formatter-date/src/formatter.test.ts — port of monolith date.test.ts; all 8 presets render correctly; raw Intl.DateTimeFormatOptions work; preset + raw merged correctly via resolvePreset; class wrapper twins helper output; cache reuse on identical key; bogus locale → UNSUPPORTED_LOCALE; Zod failure → INVALID_FORMATTER_OPTIONS
  • packages/i18n/i18n-formatter-date/src/formatter.spec-conformance.test.ts — every requirement scenario runs as an executable test
  • packages/i18n/i18n-formatter-date/src/helpers.test.ts — formatTime(d, "en-GB", false) returns short-style time; formatTime(d, "en-GB", true) includes seconds; formatDateOnly(d, "en-GB", false) short style; getDayNames("en-GB") returns 7 strings starting from Monday (en-GB getWeekInfo() → 1); getMonthNames("en-GB") returns 12 strings; getDayNames falls back to Monday-start when getWeekInfo() is unavailable
  • packages/i18n/i18n-formatter-date/src/observability.test.ts — histogram named i18n.format.date.duration_ms; counter i18n.format.cache_hit{formatter="date"} increments only on cache hit
  • packages/i18n/i18n-formatter-date/src/index.test.ts — every documented public export resolves; subpaths resolve

Impact

  • New universal-tier package — joins the rest of the i18n family.
  • Indirect consumer in Wave 6: @suaveplan/i18n slim core wires formatDate and formatTime into the default FormatBridge for ICU {date, date, short} and {ts, time, long} placeholder rendering.
  • MANIFEST.md and CLAUDE.md updated post-merge.