Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
# `@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 `display` option (`"symbol" | "code" | "name" | "narrowSymbol"`) mapped to `currencyDisplay`
Today `formatting/currency/` ships:
- `formatCurrency(amount, currency, locale, options?)` standalone (`packages/i18n/i18n/src/formatting/currency/currency.ts` L16–L29)
- `formatCurrencyValue(amount, currency, locale)` standalone — returns numeric portion only (`currency.ts` L35–L49)
- `getCurrencySymbol(currency, locale, display?)` (`currency.ts` L55–L67)
- `getCurrencyName(currency, locale)` (`currency.ts` L72–L80)
- `getCurrencyInfo(currency, locale)` returning `{code, symbol, name, decimalDigits}` (`currency.ts` L85–L98)
- BCP-47 normalisation via `LOCALE_BCP47_MAP` (`currency.ts` L4–L10)
- `CommonCurrency`, `CurrencyFormatOptions`, `CurrencyInfo` type aliases (`currency.types.ts` L1–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` — `CurrencyFormatter` class (NEW, holds stable `currency` + options); `formatCurrency` standalone (port of monolith L16–L29) extended with Zod validation, cache, and OTel emission; `formatCurrencyValue` (port of L35–L49); `clearCurrencyFormatterCache` test-only helper. Per-(locale, currency, options-hash) cache key `${locale}|${currency}|${JSON.stringify(options)}`. Currency-style construction delegates to a thin `Intl.NumberFormat` wrapper (`@suaveplan/i18n-formatter-number` exposes 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 monolith `currency.types.ts` L1–L34.
- ADDED: `src/formatter.schemas.ts` — `CurrencyCodeSchema` Zod schema validating ISO 4217 (`/^[A-Z]{3}$/`); `CurrencyFormatterOptionsSchema` validating `display`, `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 histogram `i18n.format.currency.duration_ms` + counter `i18n.format.cache_hit{formatter="currency"}`.
- ADDED: `src/formatter.test.ts` — port of monolith `currency.test.ts` (141 lines) plus new tests for `CurrencyFormatter` class, ISO 4217 validation, cache reuse.
- ADDED: `src/formatter.spec-conformance.test.ts` — every requirement scenario in this delta.
- ADDED: `src/helpers.test.ts` — covers `getCurrencySymbol`, `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` — `CurrencyFormatter` class + `formatCurrency` + `formatCurrencyValue` + types + schema only
- `./helpers` — `getCurrencySymbol`, `getCurrencyName`, `getCurrencyInfo` only
### Dependencies
- `@suaveplan/i18n-shared workspace:^` — `I18N_CODES`, `I18nError`, `LOCALE_BCP47_MAP`
- **`@suaveplan/i18n-formatter-number workspace:^`** — currency formatting is `Intl.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:observability`
- `tslib catalog:build`
- `zod catalog:forms`
- **No** dep on any other formatter package, on `@suaveplan/i18n` core, or on any backend package.
## Out of scope
- **Removing `formatting/currency/` from `packages/i18n/i18n/src/`** — happens in Wave 6.
- **Custom ISO 4217 metadata table beyond what `Intl.NumberFormat` supports** — out of scope; runtime ICU handles every active ISO 4217 code.
- **Cryptocurrency formatting** (e.g. `BTC`, `ETH` not in ISO 4217) — out of scope; falls outside `Intl.NumberFormat`'s currency table. The separate `@suaveplan/cryptocurrency` package owns crypto formatting.
- **Currency-conversion arithmetic** — out of scope; a separate `@suaveplan/currency` package 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 monolith `currency.test.ts`; cover `USD` / `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 test
- `packages/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 named `i18n.format.currency.duration_ms`; counter `i18n.format.cache_hit{formatter="currency"}` increments only on cache hit
- `packages/i18n/i18n-formatter-currency/src/index.test.ts` — every documented public export resolves; subpaths resolve; dep on `@suaveplan/i18n-formatter-number` declared in `package.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/i18n` slim core wires `formatCurrency` into the default `FormatBridge` for ICU `{amount, number, currency=USD}` placeholder rendering.
- `MANIFEST.md` and `CLAUDE.md` updated post-merge.