10 KiB
@suaveplan/i18n-formatter-currency — Intl.NumberFormat (currency style) wrapper + ISO 4217 metadata
Package: @suaveplan/i18n-formatter-currency (NEW)
Status: PROPOSED
Type: feature (new universal-tier package; verbatim port of formatting/currency/ plus class wrapper + Zod schema + observability uplift)
Date: 2026-05-05
Wave: 3 (depends on Wave 1 foundation AND on @suaveplan/i18n-formatter-number; required by Wave 6 core slim)
Design doc: openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md (especially §5 formatter package shape, §3 — currency is the only formatter that depends on a sibling formatter)
Path: packages/i18n/i18n-formatter-currency/
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-currency is the only Wave-3 formatter that depends on another formatter — @suaveplan/i18n-formatter-number — because Intl.NumberFormat with style: "currency" is the canonical currency-formatting API. Rather than duplicate the per-(locale, options-hash) cache + Zod surface from i18n-formatter-number, this package delegates the underlying Intl.NumberFormat construction to its sibling and adds a thin currency-specific layer on top:
- ISO 4217 currency-code validation (length 3, uppercase letters)
- Currency-symbol / currency-name / currency-info metadata extraction via
formatToParts - A
displayoption ("symbol" | "code" | "name" | "narrowSymbol") mapped tocurrencyDisplay
Today formatting/currency/ ships:
formatCurrency(amount, currency, locale, options?)standalone (packages/i18n/i18n/src/formatting/currency/currency.tsL16–L29)formatCurrencyValue(amount, currency, locale)standalone — returns numeric portion only (currency.tsL35–L49)getCurrencySymbol(currency, locale, display?)(currency.tsL55–L67)getCurrencyName(currency, locale)(currency.tsL72–L80)getCurrencyInfo(currency, locale)returning{code, symbol, name, decimalDigits}(currency.tsL85–L98)- BCP-47 normalisation via
LOCALE_BCP47_MAP(currency.tsL4–L10) CommonCurrency,CurrencyFormatOptions,CurrencyInfotype aliases (currency.types.tsL1–L34)
The split adds the CurrencyFormatter class wrapper, Zod-validated options (including ISO 4217 code validation), per-(locale, currency, options-hash) cache, and OTel histogram + cache-hit counter for parity with the rest of the family.
What changes
New package: @suaveplan/i18n-formatter-currency
- ADDED:
packages/i18n/i18n-formatter-currency/package scaffold (package.json,tsconfig.json,vite.config.ts,README.md). - ADDED:
src/formatter.ts—CurrencyFormatterclass (NEW, holds stablecurrency+ options);formatCurrencystandalone (port of monolith L16–L29) extended with Zod validation, cache, and OTel emission;formatCurrencyValue(port of L35–L49);clearCurrencyFormatterCachetest-only helper. Per-(locale, currency, options-hash) cache key${locale}|${currency}|${JSON.stringify(options)}. Currency-style construction delegates to a thinIntl.NumberFormatwrapper (@suaveplan/i18n-formatter-numberexposes the same Zod option-validation pattern this package mirrors for currency-specific options). - ADDED:
src/formatter.types.ts—CommonCurrency(ISO 4217 alias union),CurrencyDisplay("symbol" | "code" | "name" | "narrowSymbol"),CurrencyFormatterOptions,CurrencyInfo,CurrencyFormatterCacheKey. Sourced from monolithcurrency.types.tsL1–L34. - ADDED:
src/formatter.schemas.ts—CurrencyCodeSchemaZod schema validating ISO 4217 (/^[A-Z]{3}$/);CurrencyFormatterOptionsSchemavalidatingdisplay,minimumFractionDigits,maximumFractionDigits,locale. NEW. - ADDED:
src/helpers.ts—getCurrencySymbol(currency, locale, display?)(port of monolith L55–L67);getCurrencyName(currency, locale)(port of L72–L80);getCurrencyInfo(currency, locale)(port of L85–L98). - ADDED:
src/observability.ts— local OTel histogrami18n.format.currency.duration_ms+ counteri18n.format.cache_hit{formatter="currency"}. - ADDED:
src/formatter.test.ts— port of monolithcurrency.test.ts(141 lines) plus new tests forCurrencyFormatterclass, ISO 4217 validation, cache reuse. - ADDED:
src/formatter.spec-conformance.test.ts— every requirement scenario in this delta. - ADDED:
src/helpers.test.ts— coversgetCurrencySymbol,getCurrencyName,getCurrencyInfo. - 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—CurrencyFormatterclass +formatCurrency+formatCurrencyValue+ types + schema only./helpers—getCurrencySymbol,getCurrencyName,getCurrencyInfoonly
Dependencies
@suaveplan/i18n-shared workspace:^—I18N_CODES,I18nError,LOCALE_BCP47_MAP@suaveplan/i18n-formatter-number workspace:^— currency formatting isIntl.NumberFormat({ style: "currency", ... }); this package mirrors the Zod-validation + cache + OTel pattern from its sibling so consumers get a uniform surface@suaveplan/types workspace:^@suaveplan/error workspace:^@opentelemetry/api catalog:observabilitytslib catalog:buildzod catalog:forms- No dep on any other formatter package, on
@suaveplan/i18ncore, or on any backend package.
Out of scope
- Removing
formatting/currency/frompackages/i18n/i18n/src/— happens in Wave 6. - Custom ISO 4217 metadata table beyond what
Intl.NumberFormatsupports — out of scope; runtime ICU handles every active ISO 4217 code. - Cryptocurrency formatting (e.g.
BTC,ETHnot in ISO 4217) — out of scope; falls outsideIntl.NumberFormat's currency table. The separate@suaveplan/cryptocurrencypackage owns crypto formatting. - Currency-conversion arithmetic — out of scope; a separate
@suaveplan/currencypackage owns conversion.
Source mapping
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
formatCurrency standalone |
packages/i18n/i18n/src/formatting/currency/currency.ts |
L16–L29 | Verbatim port; extended with Zod validation + cache + OTel emission |
formatCurrencyValue standalone |
packages/i18n/i18n/src/formatting/currency/currency.ts |
L35–L49 | Verbatim port |
getCurrencySymbol |
packages/i18n/i18n/src/formatting/currency/currency.ts |
L55–L67 | Verbatim port |
getCurrencyName |
packages/i18n/i18n/src/formatting/currency/currency.ts |
L72–L80 | Verbatim port |
getCurrencyInfo |
packages/i18n/i18n/src/formatting/currency/currency.ts |
L85–L98 | Verbatim port |
toBcp47 BCP-47 normalisation |
packages/i18n/i18n/src/formatting/currency/currency.ts |
L4–L10 | Verbatim port; use LOCALE_BCP47_MAP from @suaveplan/i18n-shared |
CommonCurrency, CurrencyFormatOptions, CurrencyInfo types |
packages/i18n/i18n/src/formatting/currency/currency.types.ts |
L1–L34 | Verbatim port; rename CurrencyFormatOptions → CurrencyFormatterOptions for symmetry |
Requirements with no source line
| Requirement | Justification |
|---|---|
CurrencyFormatter class wrapper |
New — symmetric with NumberFormatter, RelativeTimeFormatter, ListFormatter, DisplayNameFormatter, DateFormatter |
ISO 4217 CurrencyCodeSchema |
New — current monolith does not validate that the currency string matches ^[A-Z]{3}$; bogus codes today silently produce Intl.NumberFormat errors that surface as raw RangeError instead of structured INVALID_FORMATTER_OPTIONS |
CurrencyFormatterOptionsSchema Zod schema |
New — current monolith does not validate options |
INVALID_FORMATTER_OPTIONS mapping |
Bound to the new Zod schemas |
| Per-(locale, currency, options-hash) cache | New — current monolith constructs a fresh Intl.NumberFormat per call; Wave-3 formatter shape mandates the cache for parity |
OTel histogram i18n.format.currency.duration_ms + cache-hit counter |
New — canonical Wave-3 formatter shape mandates these |
Dep on @suaveplan/i18n-formatter-number |
Currency is Intl.NumberFormat with style: "currency"; the design doc §3 explicitly lists this dep so the cache + Zod patterns can be shared rather than duplicated |
Verification approach
packages/i18n/i18n-formatter-currency/src/formatter.test.ts— port of monolithcurrency.test.ts; coverUSD/GBP/EUR/JPY(zero decimal digits) /CHF; symbol / code / name / narrowSymbol display; min/max fraction digits; class wrapper twins helper output; ISO 4217 violation ("$","abc","USDT") →INVALID_FORMATTER_OPTIONS; bogus locale →UNSUPPORTED_LOCALE; cache reuse on identical(locale, currency, options)packages/i18n/i18n-formatter-currency/src/formatter.spec-conformance.test.ts— every requirement scenario runs as an executable testpackages/i18n/i18n-formatter-currency/src/helpers.test.ts—getCurrencySymbol("USD", "en-GB")returns"US$"(en-GB locale);getCurrencyName("USD", "en-GB")returns"US Dollar";getCurrencyInfo("USD", "en-GB")returns{code: "USD", symbol, name, decimalDigits: 2}packages/i18n/i18n-formatter-currency/src/observability.test.ts— histogram namedi18n.format.currency.duration_ms; counteri18n.format.cache_hit{formatter="currency"}increments only on cache hitpackages/i18n/i18n-formatter-currency/src/index.test.ts— every documented public export resolves; subpaths resolve; dep on@suaveplan/i18n-formatter-numberdeclared inpackage.json
Impact
- New universal-tier package — joins the rest of the i18n family.
- Only Wave-3 package with an intra-formatter dep (on
@suaveplan/i18n-formatter-number). - Indirect consumer in Wave 6:
@suaveplan/i18nslim core wiresformatCurrencyinto the defaultFormatBridgefor ICU{amount, number, currency=USD}placeholder rendering. MANIFEST.mdandCLAUDE.mdupdated post-merge.