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,142 @@
# FormatBridge
## Purpose
`FormatBridge` is the dependency-injection seam that lets
`@suaveplan/i18n-icu-parser` render ICU format placeholders
(`{value, number}`, `{amount, number, currency=USD}`,
`{date, date, short}`, `{ts, time, long}`,
`{delta, relativeTime}`) without statically depending on any
formatter package. The renderer (`renderNodes`) consults whichever
methods are present on the bridge and falls back to
`String(value)` when a method (or the bridge itself) is undefined.
This keeps the parser package a true universal leaf with no
`Intl.*` runtime cost when the consumer doesn't need locale-aware
formatting.
The slim `@suaveplan/i18n` core (Wave 6) constructs a default
bridge by importing the formatter packages
(`@suaveplan/i18n-formatter-number`, `-date`, `-currency`,
`-relative-time`) and threads it through every render call so
consumers who use `createI18n({ ... })` get full ICU output with
zero wiring. Consumers who construct a `MessageParser` directly
MAY pass their own bridge or accept the no-Intl fallback.
## Features
- Five method slots, every one OPTIONAL: `number`, `currency`,
`date`, `time`, `relativeTime`
- Renderer never throws on missing bridge or missing method —
always falls back to `String(value)` (or
`${currency} ${String(value)}` for currency)
- Style enums (`NumberFormatStyle`, `DateStyle`,
`RelativeTimeStyle`) declared in `format-bridge.types.ts` for
type-only imports without runtime cost
- `noopFormatBridge` exported as a frozen singleton consumers can
use directly or spread-and-extend
- Single-file subpath export (`@suaveplan/i18n-icu-parser/parser/format-bridge`)
so the slim core's wiring imports only this one module — keeps
bundle size minimal
## Usage: Basic Example
```typescript
import { renderNodes, type FormatBridge } from "@suaveplan/i18n-icu-parser/parser";
const bridge: FormatBridge = {
number: (value, locale) =>
new Intl.NumberFormat(locale).format(value),
};
const nodes = [
{ type: "text" as const, value: "Total: " },
{
type: "typed-argument" as const,
name: "amount",
argType: "number" as const,
style: "decimal",
},
];
const text = renderNodes(nodes, { amount: 1234.5 }, {
locale: "en-US",
formatBridge: bridge,
});
// "Total: 1,234.5"
```
## Usage: Advanced Example
```typescript
import {
MessageParser,
noopFormatBridge,
type FormatBridge,
} from "@suaveplan/i18n-icu-parser/parser";
// Spread-and-extend pattern: keep the no-op fallback for anything
// you don't care about, override just the methods you need.
const bridge: FormatBridge = {
...noopFormatBridge,
currency: (value, currency, locale) =>
new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(value),
date: (value, locale, style = "medium") => {
const d = value instanceof Date ? value : new Date(value);
const presets: Record<string, Intl.DateTimeFormatOptions> = {
short: { dateStyle: "short" },
medium: { dateStyle: "medium" },
long: { dateStyle: "long" },
full: { dateStyle: "full" },
};
return new Intl.DateTimeFormat(locale, presets[style]).format(d);
},
};
const parser = new MessageParser();
const out = parser.format(
"Total: {amount, number, currency/USD} on {when, date, long}",
{ amount: 1234.5, when: new Date("2026-05-05T00:00:00Z") },
{ locale: "en-US", formatBridge: bridge }
);
```
## API Reference
### `interface FormatBridge`
| Method | Signature | Notes |
|---|---|---|
| `number?` | `(value, locale, style?) => string` | Style is `NumberFormatStyle \| string \| undefined` so notation styles (`compact`, `scientific`) pass through as raw strings |
| `currency?` | `(value, currency, locale) => string` | `currency` is an ISO 4217 code |
| `date?` | `(value, locale, style?) => string` | Default style is `medium` |
| `time?` | `(value, locale, longForm?) => string` | `longForm: true` should include seconds |
| `relativeTime?` | `(value, locale, style?) => string` | Style is `"long" \| "short" \| "narrow"` |
### `noopFormatBridge: Readonly<FormatBridge>`
Frozen identity bridge. Every method coerces inputs to
`String(value)` (or `${currency} ${String(value)}` for
`currency`). Safe to share by reference; mutation throws TypeError.
## Implementation Notes
- The bridge is intentionally OPTIONAL across the entire surface
area. The renderer never asserts `formatBridge != null`; it
uses optional chaining (`bridge?.number?.(...)`) and falls
through to `String(value)` on undefined results. This means a
partial bridge (only `number` defined) still renders date and
currency placeholders — they just emit raw `String(value)` for
the missing methods.
- `noopFormatBridge` is `Object.freeze`d at module load. This
guards against accidental mutation in tests or downstream
packages corrupting the shared singleton.
- `currency` returns `${code} ${String(value)}` rather than
`String(value)` alone so the no-op fallback still surfaces the
ISO 4217 code in test output, making fallback hits visible
during development.
- The style enums are declared in `format-bridge.types.ts` to keep
this implementation file zero-runtime-cost when imported for
the bridge interface alone.