9.1 KiB
@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.tsL64–L73)formatTime(date, locale, long?)standalone (date.tsL79–L83) — short / long presetformatDateOnly(date, locale, long?)standalone (date.tsL89–L97)formatDateRange(start, end, locale, options?)standalone usingformatRange(date.tsL104–L116)getDayNames(locale, style?)ECMA-402-§10.6-getWeekInfo()-aware day-name list (date.tsL126–L160)getMonthNames(locale, style?)(date.tsL166–L180)- 8-preset table (
PRESET_MAPat L4–L41):short,medium,long,full,time-short,time-long,date-short,date-long - BCP-47 normalisation via
LOCALE_BCP47_MAPfrom shared constants (date.tsL43–L49) - Type definitions (
date.types.tsL1–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—DateFormatterclass (NEW);formatDatestandalone (port of monolith L64–L73) extended with cache and OTel emission;formatDateRangestandalone (port of L104–L116);clearDateFormatterCachetest-only helper.PRESET_MAPandresolvePresethelper (port of L4–L58).toBcp47helper (port of L43–L49). - ADDED:
src/formatter.types.ts—DateFormatPreset,DateFormatOptions,ExtendedDateOptions,DateFormatterCacheKey. Sourced from monolithdate.types.tsL1–L24 plus new types for the class surface. - ADDED:
src/formatter.schemas.ts—DateFormatterOptionsSchemaZod 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 histogrami18n.format.date.duration_ms+ counteri18n.format.cache_hit{formatter="date"}. - ADDED:
src/formatter.test.ts— port of monolithdate.test.ts(185 lines) plus new tests forDateFormatterclass, Zod-validation, cache reuse. - ADDED:
src/formatter.spec-conformance.test.ts— every requirement scenario in this delta. - ADDED:
src/helpers.test.ts— coversformatTime,formatDateOnly,getDayNames,getMonthNames(includinggetWeekInfo()-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—DateFormatterclass +formatDate+formatDateRange+ types + schema only./helpers—formatTime,formatDateOnly,getDayNames,getMonthNamesonly
Dependencies
@suaveplan/i18n-shared workspace:^—I18N_CODES,I18nError,LOCALE_BCP47_MAP@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/date/frompackages/i18n/i18n/src/— happens in Wave 6. formatToPartsfor date output — out of scope for Wave 3; can be added as a follow-up if downstream consumers (e.g.react-data-tabledate columns) need part-by-part rendering.TemporalAPI support — out of scope;Intl.DateTimeFormatacceptsDate | numbertoday 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.DateTimeFormatsupports — 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 monolithdate.test.ts; all 8 presets render correctly; rawIntl.DateTimeFormatOptionswork; preset + raw merged correctly viaresolvePreset; class wrapper twins helper output; cache reuse on identical key; bogus locale →UNSUPPORTED_LOCALE; Zod failure →INVALID_FORMATTER_OPTIONSpackages/i18n/i18n-formatter-date/src/formatter.spec-conformance.test.ts— every requirement scenario runs as an executable testpackages/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-GBgetWeekInfo()→ 1);getMonthNames("en-GB")returns 12 strings;getDayNamesfalls back to Monday-start whengetWeekInfo()is unavailablepackages/i18n/i18n-formatter-date/src/observability.test.ts— histogram namedi18n.format.date.duration_ms; counteri18n.format.cache_hit{formatter="date"}increments only on cache hitpackages/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/i18nslim core wiresformatDateandformatTimeinto the defaultFormatBridgefor ICU{date, date, short}and{ts, time, long}placeholder rendering. MANIFEST.mdandCLAUDE.mdupdated post-merge.