first commit
This commit is contained in:
128
docs/localization/i18n/i18n-formatter-date/README.md
Normal file
128
docs/localization/i18n/i18n-formatter-date/README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# @suaveplan/i18n-formatter-date - Intl.DateTimeFormat wrapper for the @suaveplan/i18n family — 8-preset table (short/medium/long/full/time-short/time-long/date-short/date-long), formatDate / formatTime / formatDateOnly / formatDateRange / getDayNames / getMonthNames. Per-(locale, options-hash) cache, Zod-validated options, OTel histogram + cache-hit counter, structured I18nError mapping. Universal leaf package.
|
||||
|
||||
## Summary
|
||||
|
||||
Wrapper around `Intl.DateTimeFormat` for the `@suaveplan/i18n` family
|
||||
— 8-preset table (short/medium/long/full × date+time/time-only/date-only),
|
||||
range rendering, day/month name lists, and a per-(locale,
|
||||
options-hash) cache.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [day-month-names](./src/day-month-names/day-month-names.md) — `getDayNames(locale, style?)` and `getMonthNames(locale, style?)` return the locale-correct list of weekday or month na…
|
||||
- [day-month-names](./src/day-month-names/day-month-names.md) — `getDayNames(locale, style?)` and `getMonthNames(locale, style?)` return the locale-correct list of weekday or month na…
|
||||
- [formatter](./src/formatter/formatter.md) — `DateFormatter` and the `formatDate` / `formatDateRange` standalone helpers expose `Intl.DateTimeFormat` for the `@suav…
|
||||
- [formatter](./src/formatter/formatter.md) — `DateFormatter` and the `formatDate` / `formatDateRange` standalone helpers expose `Intl.DateTimeFormat` for the `@suav…
|
||||
- [helpers](./src/helpers/helpers.md) — `formatTime` and `formatDateOnly` pre-bind `formatDate` with the appropriate preset for time-only or date-only renderin…
|
||||
- [helpers](./src/helpers/helpers.md) — `formatTime` and `formatDateOnly` pre-bind `formatDate` with the appropriate preset for time-only or date-only renderin…
|
||||
- [observability](./src/observability/observability.md) — The Date formatter exports two OpenTelemetry instruments and a small in-memory state ring so spec-conformance tests can…
|
||||
- [observability](./src/observability/observability.md) — The Date formatter exports two OpenTelemetry instruments and a small in-memory state ring so spec-conformance tests can…
|
||||
- [presets](./src/presets/presets.md) — `PRESET_MAP` is the canonical 8-entry table that maps each `DateFormatPreset` discriminant to a concrete `Intl.DateTime…
|
||||
- [presets](./src/presets/presets.md) — `PRESET_MAP` is the canonical 8-entry table that maps each `DateFormatPreset` discriminant to a concrete `Intl.DateTime…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Why this exists
|
||||
|
||||
The 14-package i18n modular split (cross-cutting design
|
||||
`2026-05-05-i18n-modular-split.md`) lifts each `Intl.*` wrapper out
|
||||
of the monolith into its own sibling package. Lifting the date
|
||||
helper out of `@suaveplan/i18n` lets consumers that only need
|
||||
date/time rendering (most UI consumers) pull a single small package
|
||||
instead of the entire i18n core.
|
||||
|
||||
The package adds the canonical Wave-3 formatter shape on top of the
|
||||
monolith's standalone helpers: Zod-validated options, a per-(locale,
|
||||
options-hash) cache, OTel histogram + cache-hit counter, and the
|
||||
`DateFormatter` class wrapper for symmetry with `NumberFormatter` /
|
||||
`RelativeTimeFormatter` / `ListFormatter` / `DisplayNameFormatter`.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/i18n-formatter-date
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import {
|
||||
DateFormatter,
|
||||
formatDate,
|
||||
formatDateOnly,
|
||||
formatDateRange,
|
||||
formatTime,
|
||||
getDayNames,
|
||||
getMonthNames,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
const now = new Date(2026, 0, 1, 14, 30, 45);
|
||||
|
||||
formatDate(now, "en-GB", { preset: "short" }); // "01/01/2026, 14:30"
|
||||
formatTime(now, "en-GB", true); // "14:30:45"
|
||||
formatDateOnly(now, "en-GB", true); // "1 January 2026"
|
||||
|
||||
formatDateRange(
|
||||
new Date(2026, 0, 1),
|
||||
new Date(2026, 0, 5),
|
||||
"en-GB",
|
||||
{ day: "numeric", month: "long" }
|
||||
); // "1 – 5 January"
|
||||
|
||||
getDayNames("en-GB", "short");
|
||||
// ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
|
||||
const f = new DateFormatter({ preset: "long", timeZone: "Europe/London" });
|
||||
f.format(now, "en-GB");
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
| Subpath | Description |
|
||||
|---|---|
|
||||
| `@suaveplan/i18n-formatter-date` | Root barrel — class + standalones + helpers + presets + day/month names |
|
||||
| `@suaveplan/i18n-formatter-date/formatter` | `DateFormatter`, `formatDate`, `formatDateRange`, `toBcp47`, types, schema |
|
||||
| `@suaveplan/i18n-formatter-date/helpers` | `formatTime`, `formatDateOnly` |
|
||||
| `@suaveplan/i18n-formatter-date/presets` | `PRESET_MAP`, `DATE_FORMAT_PRESETS`, `resolvePreset` |
|
||||
| `@suaveplan/i18n-formatter-date/day-month-names` | `getDayNames`, `getMonthNames` |
|
||||
|
||||
## Modules Documentation
|
||||
|
||||
- [formatter.md](src/formatter.md) — `DateFormatter`, `formatDate`, `formatDateRange`, `toBcp47`, `clearDateFormatterCache`
|
||||
- [helpers.md](src/helpers.md) — `formatTime`, `formatDateOnly`
|
||||
- [presets.md](src/presets.md) — 8-preset table + `resolvePreset`
|
||||
- [day-month-names.md](src/day-month-names.md) — `getDayNames`, `getMonthNames`
|
||||
- [observability.md](src/observability.md) — OTel histogram + cache-hit counter
|
||||
|
||||
## Public Surface (snapshot)
|
||||
|
||||
| Export | Kind | Origin |
|
||||
|---|---|---|
|
||||
| `DateFormatter` | class | `formatter.ts` |
|
||||
| `formatDate` | function | `formatter.ts` |
|
||||
| `formatDateRange` | function | `formatter.ts` |
|
||||
| `toBcp47` | function | `formatter.ts` |
|
||||
| `clearDateFormatterCache` | function | `formatter.ts` |
|
||||
| `DateFormatterOptionsSchema` | Zod schema | `formatter.schemas.ts` |
|
||||
| `DateFormatPreset` | type | `formatter.types.ts` |
|
||||
| `DateFormatOptions` | type | `formatter.types.ts` |
|
||||
| `ExtendedDateOptions` | type | `formatter.types.ts` |
|
||||
| `DateFormatterCacheKey` | type | `formatter.types.ts` |
|
||||
| `formatTime` | function | `helpers.ts` |
|
||||
| `formatDateOnly` | function | `helpers.ts` |
|
||||
| `PRESET_MAP` | const | `presets.ts` |
|
||||
| `DATE_FORMAT_PRESETS` | const | `presets.ts` |
|
||||
| `resolvePreset` | function | `presets.ts` |
|
||||
| `PresetTable` | type | `presets.types.ts` |
|
||||
| `getDayNames` | function | `day-month-names.ts` |
|
||||
| `getMonthNames` | function | `day-month-names.ts` |
|
||||
| `recordDateFormat` | function | `observability.ts` |
|
||||
| `recordDateCacheHit` | function | `observability.ts` |
|
||||
| `getDateMetricsState` | function | `observability.ts` |
|
||||
| `resetDateMetricsState` | function | `observability.ts` |
|
||||
|
||||
## Versioning
|
||||
|
||||
Part of the 14-package i18n modular-split rollout. Stays at `0.1.0`
|
||||
until the slim core (Wave 6) cuts the family `1.0.0` release.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Day / Month names
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`getDayNames(locale, style?)` and `getMonthNames(locale, style?)`
|
||||
return the locale-correct list of weekday or month names. They are
|
||||
the canonical primitives for day-picker / calendar / date-picker UI
|
||||
components — every locale wants a different starting weekday and a
|
||||
different rendering style.
|
||||
|
||||
`getDayNames` consults `Intl.Locale.getWeekInfo()` (ECMA-402 §10.6) to
|
||||
discover the locale-defined first day of the week (1 = Monday … 7 =
|
||||
Sunday). When the host runtime lacks `getWeekInfo` (older Node, older
|
||||
browsers), the function falls back to Monday-start.
|
||||
|
||||
## Features
|
||||
|
||||
- `getDayNames(locale, style?)` returns 7 strings starting from the locale's first weekday
|
||||
- `getMonthNames(locale, style?)` returns 12 strings
|
||||
- Both accept `"long"` (default), `"short"`, or `"narrow"` style
|
||||
- BCP-47 normalisation through `LOCALE_BCP47_MAP`
|
||||
- `getDayNames` falls back to Monday-start when `getWeekInfo` is unavailable or throws
|
||||
- Pure functions — no shared state, no cache
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
getDayNames,
|
||||
getMonthNames,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
getDayNames("en-GB");
|
||||
// ["Monday", "Tuesday", ..., "Sunday"]
|
||||
|
||||
getDayNames("en-US", "short");
|
||||
// ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
|
||||
getMonthNames("en-GB");
|
||||
// ["January", "February", ..., "December"]
|
||||
|
||||
getMonthNames("fr-FR", "narrow");
|
||||
// ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
getDayNames,
|
||||
getMonthNames,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
// Build a date-picker header that respects the user's locale.
|
||||
function renderCalendarHeader(locale: string) {
|
||||
const days = getDayNames(locale, "narrow");
|
||||
const months = getMonthNames(locale, "long");
|
||||
return { days, months };
|
||||
}
|
||||
|
||||
const header = renderCalendarHeader("ja-JP");
|
||||
// header.days[0] is the locale-correct first weekday name
|
||||
// header.months is the 12 long Japanese month names
|
||||
|
||||
// Stub Intl.Locale.getWeekInfo to test the Monday-start fallback.
|
||||
const Original = Intl.Locale;
|
||||
Intl.Locale = function () {
|
||||
return { getWeekInfo: undefined } as unknown as Intl.Locale;
|
||||
} as unknown as typeof Intl.Locale;
|
||||
try {
|
||||
const days = getDayNames("en-US"); // falls back to Monday-start
|
||||
console.log(days[0]); // "Monday"
|
||||
} finally {
|
||||
Intl.Locale = Original;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getDayNames(locale, style?): string[]`
|
||||
|
||||
- `locale` — BCP-47 locale tag.
|
||||
- `style` — `"long"` (default), `"short"`, or `"narrow"`.
|
||||
- Returns 7 strings starting from the locale-defined first weekday.
|
||||
- Anchors on Monday 2024-01-01 then offsets by `(firstDay - 1)` so
|
||||
index 0 is the locale's first weekday.
|
||||
|
||||
### `getMonthNames(locale, style?): string[]`
|
||||
|
||||
- `locale` — BCP-47 locale tag.
|
||||
- `style` — `"long"` (default), `"short"`, or `"narrow"`.
|
||||
- Returns 12 strings, one per month.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `getWeekInfo()` probe is wrapped in `try/catch` because (a)
|
||||
older runtimes don't expose the method at all (the access throws
|
||||
`TypeError`), and (b) some older proposals exposed `weekInfo` as a
|
||||
property — neither path is hit on current Node/Bun/browser, but the
|
||||
fallback keeps the function safe across the entire range.
|
||||
- The constants are computed by iterating dates through
|
||||
`Intl.DateTimeFormat`, not by hard-coding day/month strings —
|
||||
that's what makes the helpers correctly return localised names for
|
||||
every BCP-47 tag without a translation table.
|
||||
- These helpers do NOT participate in the formatter cache because
|
||||
they construct ad-hoc `Intl.DateTimeFormat` instances per call.
|
||||
The cost is negligible (single-digit microseconds) and avoids
|
||||
mixing weekday-style cache entries with the main formatter cache.
|
||||
@@ -0,0 +1,133 @@
|
||||
# DateFormatter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`DateFormatter` and the `formatDate` / `formatDateRange` standalone
|
||||
helpers expose `Intl.DateTimeFormat` for the `@suaveplan/i18n` family
|
||||
at the canonical Wave-3 formatter surface. Consumers format `Date`
|
||||
instances against an 8-entry preset table or raw
|
||||
`Intl.DateTimeFormatOptions`, with locale-correct rendering across
|
||||
every BCP-47 tag.
|
||||
|
||||
This module is the broadest formatter in the family — it ships the
|
||||
class wrapper, the standalone formatters, the preset/raw merge logic,
|
||||
the BCP-47 normalisation map (via `@suaveplan/i18n-shared`), and the
|
||||
per-(locale, options-hash) cache.
|
||||
|
||||
## Features
|
||||
|
||||
- `DateFormatter` class with stored options + `format(date, locale)` and `formatRange(start, end, locale)` methods
|
||||
- `formatDate(date, locale, options?)` standalone helper
|
||||
- `formatDateRange(start, end, locale, options?)` standalone helper
|
||||
- 8-preset table: `short`, `medium`, `long`, `full`, `time-short`, `time-long`, `date-short`, `date-long`
|
||||
- Raw `Intl.DateTimeFormatOptions` accepted alongside or instead of `preset`
|
||||
- Construction-time Zod validation with `INVALID_FORMATTER_OPTIONS` mapping
|
||||
- `Intl.DateTimeFormat` `RangeError` mapped to `UNSUPPORTED_LOCALE` with original error attached as `cause`
|
||||
- Per-(locale, options-hash) cache — repeated calls with identical key reuse the same `Intl.DateTimeFormat`
|
||||
- BCP-47 normalisation through `LOCALE_BCP47_MAP` from `@suaveplan/i18n-shared`
|
||||
- `clearDateFormatterCache()` test-only helper
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
formatDate,
|
||||
formatDateRange,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
formatDate(new Date(2026, 0, 1), "en-GB", { preset: "short" });
|
||||
// e.g. "01/01/2026, 00:00"
|
||||
|
||||
formatDate(new Date(2026, 0, 1), "en-GB", { preset: "long" });
|
||||
// e.g. "1 January 2026 at 00:00:00"
|
||||
|
||||
formatDateRange(
|
||||
new Date(2026, 0, 1),
|
||||
new Date(2026, 0, 5),
|
||||
"en-GB",
|
||||
{ day: "numeric", month: "long" }
|
||||
); // e.g. "1 – 5 January"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DateFormatter,
|
||||
type ExtendedDateOptions,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
const opts: ExtendedDateOptions = {
|
||||
preset: "short",
|
||||
// Override the preset's month field — raw options take precedence.
|
||||
month: "long",
|
||||
timeZone: "Europe/London",
|
||||
};
|
||||
const f = new DateFormatter(opts);
|
||||
|
||||
f.format(new Date(2026, 0, 1), "en-GB"); // "1 January 2026, 00:00"
|
||||
f.format(new Date(2026, 0, 1), "fr-FR"); // "1 janvier 2026 à 00:00"
|
||||
|
||||
// Range rendering uses the same configured options.
|
||||
f.formatRange(
|
||||
new Date(2026, 0, 1),
|
||||
new Date(2026, 0, 5),
|
||||
"en-GB"
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `formatDate(date, locale, options?): string`
|
||||
|
||||
Standalone helper. Validates options via Zod, normalises the locale
|
||||
through `LOCALE_BCP47_MAP`, resolves `options.preset` against
|
||||
`PRESET_MAP` (with raw fields overriding preset defaults), looks up
|
||||
the cached `Intl.DateTimeFormat`, and returns `formatter.format(date)`.
|
||||
|
||||
Throws `I18nError(INVALID_FORMATTER_OPTIONS)` on Zod failure; throws
|
||||
`I18nError(UNSUPPORTED_LOCALE)` on `Intl.DateTimeFormat` `RangeError`
|
||||
with the original error as `cause`.
|
||||
|
||||
### `formatDateRange(start, end, locale, options?): string`
|
||||
|
||||
Standalone helper for range rendering. Uses
|
||||
`Intl.DateTimeFormat.formatRange` under the hood; raw
|
||||
`Intl.DateTimeFormatOptions` are passed straight through (no preset
|
||||
resolution).
|
||||
|
||||
### `class DateFormatter`
|
||||
|
||||
- `constructor(options?: ExtendedDateOptions)` — Zod-validates options at construction
|
||||
- `format(date: Date, locale: string): string` — same semantics as standalone
|
||||
- `formatRange(start: Date, end: Date, locale: string): string` — uses the configured options' resolved Intl options for range rendering
|
||||
|
||||
### `toBcp47(locale: string): string`
|
||||
|
||||
Look up the locale in `LOCALE_BCP47_MAP` (direct first, then via
|
||||
`toLowerCase` fallback). Returns the input locale unchanged when no
|
||||
mapping exists. Exported for downstream callers (e.g. day/month name
|
||||
helpers).
|
||||
|
||||
### `clearDateFormatterCache(): void`
|
||||
|
||||
Test-only helper that empties the per-(locale, options-hash) cache.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The cache key is `${locale}|${JSON.stringify(options)}` — JSON
|
||||
serialisation is stable for objects with no symbol/function keys
|
||||
and no NaN/Infinity values, which `ExtendedDateOptions` does not
|
||||
contain.
|
||||
- `presetTag(options)` returns `options.preset ?? "custom"` so the
|
||||
OTel histogram tag is always present.
|
||||
- The `formatRange` method on `Intl.DateTimeFormat` ships in Node 18+
|
||||
and every current browser. The package does not provide a
|
||||
fallback — a runtime missing `formatRange` is below the family's
|
||||
documented support floor.
|
||||
- `validateOptions(undefined)` is a no-op so consumers can call
|
||||
`formatDate(d, l)` with no options object.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Date helpers — `formatTime`, `formatDateOnly`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`formatTime` and `formatDateOnly` pre-bind `formatDate` with the
|
||||
appropriate preset for time-only or date-only rendering. They exist
|
||||
because the most common UI patterns ("show just the time", "show just
|
||||
the date") shouldn't require the caller to remember the preset
|
||||
discriminant string.
|
||||
|
||||
## Features
|
||||
|
||||
- `formatTime(date, locale, long?)` — pre-binds `time-short` (`long === false`) or `time-long` (`long === true`)
|
||||
- `formatDateOnly(date, locale, long?)` — pre-binds `date-short` (`long === false`) or `date-long` (`long === true`)
|
||||
- Both default `long` to `false`
|
||||
- Both go through the cached `formatDate` path — every advantage of the per-(locale, options-hash) cache and OTel emission applies
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
formatDateOnly,
|
||||
formatTime,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
const now = new Date(2026, 0, 1, 14, 30, 45);
|
||||
|
||||
formatTime(now, "en-GB"); // "14:30"
|
||||
formatTime(now, "en-GB", true); // "14:30:45"
|
||||
|
||||
formatDateOnly(now, "en-GB"); // "01/01/2026"
|
||||
formatDateOnly(now, "en-GB", true); // "1 January 2026"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
formatDateOnly,
|
||||
formatTime,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
// Render a chat message timestamp — short form by default,
|
||||
// long form on hover/expand.
|
||||
function renderTimestamp(d: Date, locale: string, expanded: boolean) {
|
||||
const date = formatDateOnly(d, locale, expanded);
|
||||
const time = formatTime(d, locale, expanded);
|
||||
return expanded ? `${date} at ${time}` : `${date} ${time}`;
|
||||
}
|
||||
|
||||
renderTimestamp(new Date(2026, 5, 15, 9, 5), "en-GB", false);
|
||||
// "15/06/2026 09:05"
|
||||
|
||||
renderTimestamp(new Date(2026, 5, 15, 9, 5, 30), "en-GB", true);
|
||||
// "15 June 2026 at 09:05:30"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `formatTime(date, locale, long?): string`
|
||||
|
||||
Pre-binds `formatDate` with `preset: long ? "time-long" : "time-short"`.
|
||||
|
||||
- `date` — a `Date` instance.
|
||||
- `locale` — BCP-47 locale tag.
|
||||
- `long` — when `true`, includes seconds.
|
||||
|
||||
Throws every error class `formatDate` throws.
|
||||
|
||||
### `formatDateOnly(date, locale, long?): string`
|
||||
|
||||
Pre-binds `formatDate` with `preset: long ? "date-long" : "date-short"`.
|
||||
|
||||
- `date` — a `Date` instance.
|
||||
- `locale` — BCP-47 locale tag.
|
||||
- `long` — when `true`, uses the long month name (e.g. "January"
|
||||
instead of "01").
|
||||
|
||||
Throws every error class `formatDate` throws.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Both helpers are one-line wrappers around `formatDate`. The
|
||||
per-(locale, options-hash) cache shared with `formatDate` and
|
||||
`formatDateRange` covers them automatically.
|
||||
- Default of `long = false` matches the monolith behaviour to
|
||||
minimise behavioural drift during the modular split.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Date observability
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The Date formatter exports two OpenTelemetry instruments and a small
|
||||
in-memory state ring so spec-conformance tests can assert exact
|
||||
attribute payloads without standing up a full SDK exporter. The
|
||||
instrument names match the canonical Wave-3 formatter contract —
|
||||
every sibling formatter (number, relative-time, list, display-name,
|
||||
currency, collator, segmenter) emits the same two metric names so
|
||||
dashboards and alerts compose cleanly across the family.
|
||||
|
||||
## Features
|
||||
|
||||
- Histogram `i18n.format.date.duration_ms` (unit `ms`) — per-format wall-clock time
|
||||
- Counter `i18n.format.cache_hit{formatter="date"}` — increments on cache reuse only
|
||||
- In-memory state ring (`getDateMetricsState()`) for spec-conformance
|
||||
- `resetDateMetricsState()` test-only helper to clear the ring
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
const provider = new MeterProvider({ readers: [/* reader */] });
|
||||
metrics.setGlobalMeterProvider(provider);
|
||||
|
||||
import { formatDate } from "@suaveplan/i18n-formatter-date";
|
||||
formatDate(new Date(2026, 0, 1), "en-GB", { preset: "short" });
|
||||
// emits via your reader
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
clearDateFormatterCache,
|
||||
formatDate,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
import {
|
||||
getDateMetricsState,
|
||||
resetDateMetricsState,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
it("emits histogram + cache-hit counter", () => {
|
||||
clearDateFormatterCache();
|
||||
resetDateMetricsState();
|
||||
formatDate(new Date(2026, 0, 1), "en-GB", { preset: "short" });
|
||||
formatDate(new Date(2026, 0, 1), "en-GB", { preset: "short" });
|
||||
const state = getDateMetricsState();
|
||||
expect(state.durationSamples).toHaveLength(2);
|
||||
expect(state.cacheHitSamples).toHaveLength(1);
|
||||
expect(state.durationSamples[0]?.attributes).toMatchObject({
|
||||
locale: "en-GB",
|
||||
preset: "short",
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `recordDateFormat(locale, preset, durationMs): void`
|
||||
|
||||
Records the duration histogram sample. Tags: `locale`, `preset`.
|
||||
Called internally by `formatDate` / `formatDateRange`.
|
||||
|
||||
### `recordDateCacheHit(): void`
|
||||
|
||||
Increments the cache-hit counter with the constant tag
|
||||
`{ formatter: "date" }`.
|
||||
|
||||
### `getDateMetricsState(): DateMetricsState`
|
||||
|
||||
Returns the in-memory ring of `durationSamples` and
|
||||
`cacheHitSamples`. Used by spec-conformance tests.
|
||||
|
||||
### `resetDateMetricsState(): void`
|
||||
|
||||
Clears both ring buffers. Test-only helper.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The OTel meter is named `suaveplan.i18n.formatter.date`. Every
|
||||
formatter in the family has its own meter so OTel resource
|
||||
attributes can be tied per-package if a consumer chooses.
|
||||
- When the caller omits `preset`, the histogram tag is `"custom"` so
|
||||
every sample carries a non-null preset attribute.
|
||||
- Sample dashboard query (PromQL/Grafana over OTLP):
|
||||
`histogram_quantile(0.99, sum(rate(i18n_format_date_duration_ms_bucket[5m])) by (le, locale, preset))`
|
||||
- The in-memory ring is intentionally unbounded — tests are short
|
||||
and call `resetDateMetricsState()` between cases. A long-running
|
||||
test loop should call reset to avoid memory growth.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Date format presets
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`PRESET_MAP` is the canonical 8-entry table that maps each
|
||||
`DateFormatPreset` discriminant to a concrete
|
||||
`Intl.DateTimeFormatOptions` payload. `resolvePreset(options)` is the
|
||||
merge function used inside `formatDate` to combine a preset's
|
||||
defaults with raw caller-supplied options (raw fields override).
|
||||
|
||||
The presets exist so consumers can render dates with ergonomic verbs
|
||||
(`{ preset: "short" }`) instead of having to memorise the eight or so
|
||||
field-style enums `Intl.DateTimeFormat` accepts.
|
||||
|
||||
## Features
|
||||
|
||||
- 8 frozen presets covering every common UI case (date+time / time-only / date-only, plus `full` for legal-style timestamps)
|
||||
- `DATE_FORMAT_PRESETS` array of all preset names — useful for building UI selects
|
||||
- `resolvePreset(options?)` merges preset defaults with raw fields; raw fields take precedence
|
||||
- Pure functions, side-effect-free
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DATE_FORMAT_PRESETS,
|
||||
PRESET_MAP,
|
||||
resolvePreset,
|
||||
} from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
// Inspect a preset directly.
|
||||
PRESET_MAP.short;
|
||||
// { year: "numeric", month: "numeric", day: "numeric",
|
||||
// hour: "numeric", minute: "numeric" }
|
||||
|
||||
// Build a select.
|
||||
DATE_FORMAT_PRESETS.map((p) => ({ value: p, label: p }));
|
||||
|
||||
// Resolve a merged options object.
|
||||
resolvePreset({ preset: "short", month: "long" });
|
||||
// { year: "numeric", month: "long", day: "numeric",
|
||||
// hour: "numeric", minute: "numeric" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { resolvePreset } from "@suaveplan/i18n-formatter-date";
|
||||
|
||||
// Power a custom Intl wrapper that needs the resolved Intl options
|
||||
// without going through formatDate.
|
||||
function customDate(d: Date, locale: string, preset: "short" | "long") {
|
||||
const opts = resolvePreset({ preset });
|
||||
return new Intl.DateTimeFormat(locale, opts).format(d);
|
||||
}
|
||||
|
||||
// Override the default fields of a preset.
|
||||
const fullWithoutWeekday = resolvePreset({
|
||||
preset: "full",
|
||||
weekday: undefined, // raw fields override; setting to undefined
|
||||
// keeps the field in the object but
|
||||
// Intl.DateTimeFormat ignores `undefined`.
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PRESET_MAP: Readonly<Record<DateFormatPreset, Intl.DateTimeFormatOptions>>`
|
||||
|
||||
The 8-entry table. Keys: `"short"`, `"medium"`, `"long"`, `"full"`,
|
||||
`"time-short"`, `"time-long"`, `"date-short"`, `"date-long"`.
|
||||
|
||||
### `DATE_FORMAT_PRESETS: readonly DateFormatPreset[]`
|
||||
|
||||
Frozen array of every preset name. Useful for select-box population.
|
||||
|
||||
### `resolvePreset(options?): Intl.DateTimeFormatOptions | undefined`
|
||||
|
||||
- Returns `undefined` when `options` is `undefined`.
|
||||
- Returns `rest` (with `preset` and `locale` stripped) when no
|
||||
`preset` is supplied.
|
||||
- Returns `{ ...PRESET_MAP[preset], ...rest }` when `preset` is
|
||||
supplied — raw fields override preset defaults.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `PRESET_MAP` and `DATE_FORMAT_PRESETS` are sourced verbatim from
|
||||
the monolith `formatting/date/date.ts` L4–L41 (per the source
|
||||
mapping table in the change folder's proposal).
|
||||
- `resolvePreset` mirrors the monolith's L51–L58. The function is
|
||||
small, pure, and exported so consumers can build their own Intl
|
||||
wrappers without reinventing the merge.
|
||||
- The `locale` field is stripped because `Intl.DateTimeFormat` does
|
||||
not accept `locale` inside the options object — it must be passed
|
||||
as the first constructor argument. Stripping here keeps the
|
||||
resolved options object passable to `Intl.DateTimeFormat` directly.
|
||||
Reference in New Issue
Block a user