11 KiB
Capability Spec: i18n-formatter-currency (i18n-formatter-currency)
Package: @suaveplan/i18n-formatter-currency
Status: ACTIVE
Every requirement below carries a
**Verified by:**line at authoring time. (Seeopenspec/AGENTS.mdSection 13.)Only Wave-3 formatter with a sibling-formatter dep — depends on
@suaveplan/i18n-formatter-number.
Requirements
Requirement: CurrencyFormatter class wrapping Intl.NumberFormat (currency style)
The package SHALL export a CurrencyFormatter class that wraps Intl.NumberFormat with style: "currency". Construction SHALL validate the currency code against ISO 4217 (/^[A-Z]{3}$/) and validate options via Zod. Construction with a malformed currency code or malformed options SHALL throw I18nError(I18N.INVALID_FORMATTER_OPTIONS).
Scenario: format with USD
- GIVEN
const f = new CurrencyFormatter("USD") - WHEN
f.format(99.99, "en-GB")is called - THEN the result equals
"US$99.99"(en-GB rendering of USD)
Scenario: bogus currency code throws INVALID_FORMATTER_OPTIONS
- WHEN
new CurrencyFormatter("usd")is called (lowercase) - THEN the call throws
I18nErrorwith code"I18N.INVALID_FORMATTER_OPTIONS"
Scenario: too-long currency code throws INVALID_FORMATTER_OPTIONS
- WHEN
new CurrencyFormatter("USDT")is called - THEN the call throws
I18nErrorwith code"I18N.INVALID_FORMATTER_OPTIONS"
Verified by: packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"CurrencyFormatter formats USD" AND packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"CurrencyFormatter rejects lowercase currency code" AND packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"CurrencyFormatter rejects too-long currency code"
Requirement: formatCurrency standalone helper
The package SHALL export a formatCurrency(amount, currency, locale, options?) standalone helper that delivers the same behaviour as CurrencyFormatter.format without requiring class construction. Defaults: display: "symbol".
Scenario: standalone helper formats GBP
- WHEN
formatCurrency(100, "GBP", "en-GB")is called - THEN the result equals
"£100.00"
Scenario: standalone helper twins class output
- WHEN both
formatCurrency(amt, cur, l, opts)andnew CurrencyFormatter(cur, opts).format(amt, l)are called for the same(amt, cur, l, opts) - THEN the results are identical
Verified by: packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"formatCurrency formats GBP" AND packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"formatCurrency output twins CurrencyFormatter.format output"
Requirement: formatCurrencyValue returns numeric portion only
The package SHALL export a formatCurrencyValue(amount, currency, locale) standalone helper that returns the numeric portion of the formatted currency without the symbol or code (uses formatToParts and filters out currency and literal parts).
Scenario: numeric portion only
- WHEN
formatCurrencyValue(99.99, "USD", "en-GB")is called - THEN the result equals
"99.99"(no"US$"prefix)
Verified by: packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"formatCurrencyValue returns numeric portion only"
Requirement: display option supports symbol / code / name / narrowSymbol
The package SHALL accept a display option mapping to Intl.NumberFormat's currencyDisplay field with the four documented values. Defaults to "symbol".
Scenario: display: code renders ISO code
- WHEN
formatCurrency(100, "USD", "en-GB", { display: "code" })is called - THEN the result includes
"USD"and the numeric value
Scenario: display: name renders currency name
- WHEN
formatCurrency(100, "USD", "en-GB", { display: "name" })is called - THEN the result includes
"US dollars"(locale-correct rendering)
Scenario: display: narrowSymbol renders compact symbol
- WHEN
formatCurrency(100, "USD", "en-GB", { display: "narrowSymbol" })is called - THEN the result starts with
"$"(narrow form, no"US"prefix)
Verified by: packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"display: code renders ISO code" AND packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"display: name renders currency name" AND packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"display: narrowSymbol renders compact symbol"
Requirement: Intl.NumberFormat RangeError maps to UNSUPPORTED_LOCALE
The package SHALL catch every RangeError thrown by the underlying Intl.NumberFormat constructor and re-throw it as I18nError(I18N.UNSUPPORTED_LOCALE) with the original error attached as cause and the locale string in context.
Scenario: bogus locale throws UNSUPPORTED_LOCALE
- WHEN
formatCurrency(1, "USD", "xx-bogus-tag-!!!")is called - THEN the call throws
I18nErrorwith code"I18N.UNSUPPORTED_LOCALE"AND the error'scauseis the originalRangeError
Verified by: packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"unsupported locale throws UNSUPPORTED_LOCALE with cause"
Requirement: Per-(locale, currency, options-hash) cache
The package SHALL maintain an internal Map<string, Intl.NumberFormat> keyed by ${locale}|${currency}|${JSON.stringify(options)}. Repeated calls with identical (locale, currency, options) SHALL reuse the same Intl.NumberFormat instance. The package SHALL export a clearCurrencyFormatterCache() test-only helper.
Scenario: identical key reuses the same Intl.NumberFormat
- GIVEN the cache is empty
- WHEN
formatCurrency(1, "USD", "en-GB")is called twice in sequence - THEN only one
Intl.NumberFormatconstructor invocation occurs
Scenario: distinct currency creates distinct cache entries
- WHEN
formatCurrency(1, "USD", "en-GB")andformatCurrency(1, "EUR", "en-GB")are called - THEN two distinct
Intl.NumberFormatinstances are created and cached
Verified by: packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"identical key reuses cached Intl.NumberFormat" AND packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"distinct currency creates distinct cache entries"
Requirement: OTel histogram i18n.format.currency.duration_ms
The package SHALL emit an OTel histogram named exactly i18n.format.currency.duration_ms (unit ms) on every successful formatCurrency or formatCurrencyValue call, tagged with locale, currency, and display.
Scenario: histogram emitted on every call
- GIVEN an OTel test exporter capturing histogram observations
- WHEN
formatCurrency(1, "USD", "en-GB", { display: "symbol" })is called - THEN exactly one observation appears under
i18n.format.currency.duration_mswithlocale,currency, anddisplayattributes
Verified by: packages/i18n/i18n-formatter-currency/src/observability.test.ts::"emits i18n.format.currency.duration_ms histogram on every call"
Requirement: OTel cache-hit counter i18n.format.cache_hit
The package SHALL emit an OTel counter i18n.format.cache_hit tagged with formatter: "currency" on every cache hit. The counter SHALL NOT increment on the first (cache-miss) call.
Scenario: cache-hit counter increments only on second call
- GIVEN an OTel test exporter capturing counter observations and an empty cache
- WHEN
formatCurrency(1, "USD", "en-GB")is called twice in sequence - THEN the counter
i18n.format.cache_hit{formatter="currency"}shows exactly one increment
Verified by: packages/i18n/i18n-formatter-currency/src/observability.test.ts::"cache-hit counter increments only on second call with same options"
Requirement: ISO 4217 metadata helpers — symbol, name, info
The package SHALL export three metadata helpers:
getCurrencySymbol(currency, locale, display?)— returns the symbol string per the configureddisplaymode (default"symbol")getCurrencyName(currency, locale)— returns the localized currency namegetCurrencyInfo(currency, locale)— returns{ code, symbol, name, decimalDigits }aggregating the previous two plus the runtime-resolved decimal-digit count
Scenario: getCurrencySymbol renders en-GB USD symbol
- WHEN
getCurrencySymbol("USD", "en-GB")is called - THEN the result equals
"US$"(en-GB locale rendering)
Scenario: getCurrencyName renders US Dollar
- WHEN
getCurrencyName("USD", "en-GB")is called - THEN the result equals
"US Dollar"
Scenario: getCurrencyInfo aggregates symbol, name, decimal digits
- WHEN
getCurrencyInfo("USD", "en-GB")is called - THEN the result equals
{ code: "USD", symbol: "US$", name: "US Dollar", decimalDigits: 2 }
Scenario: JPY has zero decimal digits
- WHEN
getCurrencyInfo("JPY", "en-GB")is called - THEN the
decimalDigitsfield equals0
Verified by: packages/i18n/i18n-formatter-currency/src/helpers.test.ts::"getCurrencySymbol renders en-GB USD symbol" AND packages/i18n/i18n-formatter-currency/src/helpers.test.ts::"getCurrencyName renders US Dollar" AND packages/i18n/i18n-formatter-currency/src/helpers.test.ts::"getCurrencyInfo aggregates symbol, name, decimal digits" AND packages/i18n/i18n-formatter-currency/src/helpers.test.ts::"getCurrencyInfo JPY has zero decimal digits"
Requirement: Subpath exports
The package SHALL declare subpath exports in package.json:
@suaveplan/i18n-formatter-currency— root barrel@suaveplan/i18n-formatter-currency/formatter—CurrencyFormatterclass +formatCurrency+formatCurrencyValue+ types + schema only@suaveplan/i18n-formatter-currency/helpers—getCurrencySymbol,getCurrencyName,getCurrencyInfoonly
Scenario: every documented subpath resolves
- WHEN every documented subpath is imported in a consumer test fixture
- THEN every import resolves with no
Module not founderrors
Verified by: packages/i18n/i18n-formatter-currency/src/index.test.ts::"every documented subpath export resolves"
Requirement: Sibling dep on @suaveplan/i18n-formatter-number
The package SHALL declare @suaveplan/i18n-formatter-number as a workspace:^ dependency in package.json. This dep is documented because currency formatting is Intl.NumberFormat({ style: "currency", ... }) — the two packages share the same Zod-validation + cache + OTel pattern.
Scenario: package.json declares dep on i18n-formatter-number
- WHEN
package.jsonis parsed - THEN
dependencies["@suaveplan/i18n-formatter-number"]equals"workspace:^"
Verified by: packages/i18n/i18n-formatter-currency/src/index.test.ts::"package.json declares dep on @suaveplan/i18n-formatter-number"
Requirement: Side-effect-free package
The package SHALL declare "sideEffects": false in package.json.
Scenario: package.json declares sideEffects false
- WHEN
package.jsonis parsed - THEN the
sideEffectsfield equalsfalse
Verified by: packages/i18n/i18n-formatter-currency/src/index.test.ts::"package.json declares sideEffects false"