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

107 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `@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.