first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

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