# 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 = { 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` 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.