Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
# @suaveplan/i18n-formatter-currency - Locale-aware currency formatting backed by Intl.NumberFormat({ style: 'currency' }). Class wrapper + standalone helper + currency-info helpers (getCurrencySymbol, getCurrencyName, getCurrencyInfo), Zod-validated ISO 4217 codes, per-(locale, currency, options-hash) cache, OTel histogram + cache-hit counter. Wave 3 of the @suaveplan/i18n modular split.
## Summary
Locale-aware currency formatting backed by `Intl.NumberFormat` with
`style: "currency"`. Wave 3 of the 14-package i18n modular split.
Provides both a class (`CurrencyFormatter`) and a standalone helper
(`formatCurrency`) plus value-only formatting (`formatCurrencyValue`)
and currency-info helpers (`getCurrencySymbol`, `getCurrencyName`,
`getCurrencyInfo`) on top of a per-`(locale, currency, options-hash)`
cache, Zod-validated ISO 4217 codes, and the canonical OTel histogram
+ cache-hit counter that every Wave 3 formatter package emits.
<!-- begin:auto-docs -->
## Documentation tree
- [formatter](./src/formatter/formatter.md) — Locale-aware currency formatting backed by `Intl.NumberFormat({ style: "currency" })`. The package exposes both a class…
- [formatter](./src/formatter/formatter.md) — Locale-aware currency formatting backed by `Intl.NumberFormat({ style: "currency" })`. The package exposes both a class…
- [helpers](./src/helpers/helpers.md) — Currency-info helpers exposing structured metadata about ISO 4217 codes via `Intl.NumberFormat.formatToParts`. Use thes…
- [helpers](./src/helpers/helpers.md) — Currency-info helpers exposing structured metadata about ISO 4217 codes via `Intl.NumberFormat.formatToParts`. Use thes…
- [observability](./src/observability/observability.md) — Per-package OpenTelemetry instruments for `@suaveplan/i18n-formatter-currency`. Exposes a histogram (`i18n.format.curre…
- [observability](./src/observability/observability.md) — Per-package OpenTelemetry instruments for `@suaveplan/i18n-formatter-currency`. Exposes a histogram (`i18n.format.curre…
<!-- end:auto-docs -->
## Why this exists
Each `Intl.*` wrapper lives in its own sibling package so consumers
can pull only the formatters they actually use. This package is the
only Wave 3 formatter with an intra-formatter dependency: it depends
on `@suaveplan/i18n-formatter-number workspace:^` because currency
formatting is `Intl.NumberFormat({ style: "currency" })`. The dep is
pinned in `package.json` per the design doc §3 so the slim core
(Wave 6) can wire `formatCurrency` and `formatNumber` consistently
into the default `FormatBridge`.
## Install
```bash
bun add @suaveplan/i18n-formatter-currency
```
## Quick Start
```ts
import {
CurrencyFormatter,
formatCurrency,
formatCurrencyValue,
getCurrencyInfo,
getCurrencyName,
getCurrencySymbol,
} from "@suaveplan/i18n-formatter-currency";
formatCurrency(1234.56, "USD", "en"); // → "$1,234.56"
formatCurrency(1234.56, "EUR", "de"); // → "1.234,56 €"
formatCurrency(100, "CHF", "en", { display: "code" }); // → "CHF 100.00"
formatCurrencyValue(9.99, "USD", "en"); // → "9.99"
getCurrencySymbol("GBP", "en"); // → "£"
getCurrencyName("USD", "fr"); // → "dollar des États-Unis"
getCurrencyInfo("EUR", "en-US");
// → { code: "EUR", symbol: "€", name: "Euro", decimalDigits: 2 }
const usd = new CurrencyFormatter("USD", { minimumFractionDigits: 2 });
usd.format(42, "en-US"); // → "$42.00"
```
## Modules
| Subpath | Description |
|---|---|
| `@suaveplan/i18n-formatter-currency` | Root barrel — class + standalone + helpers + observability |
| `@suaveplan/i18n-formatter-currency/formatter` | `CurrencyFormatter`, `formatCurrency`, `formatCurrencyValue`, types, schemas |
| `@suaveplan/i18n-formatter-currency/helpers` | `getCurrencySymbol`, `getCurrencyName`, `getCurrencyInfo` only |
## Modules Documentation
+ [formatter.md](src/formatter.md) — `CurrencyFormatter`, `formatCurrency`, `formatCurrencyValue`
+ [helpers.md](src/helpers.md) — `getCurrencySymbol`, `getCurrencyName`, `getCurrencyInfo`
+ [observability.md](src/observability.md) — OTel histogram + cache-hit counter
## Public Surface (snapshot)
| Export | Kind | Origin |
|---|---|---|
| `CurrencyFormatter` | class | `formatter.ts` |
| `formatCurrency` | function | `formatter.ts` |
| `formatCurrencyValue` | function | `formatter.ts` |
| `clearCurrencyFormatterCache` | function | `formatter.ts` |
| `toBcp47` | function | `formatter.ts` |
| `CurrencyCodeSchema` | const | `formatter.schemas.ts` |
| `CurrencyFormatterOptionsSchema` | const | `formatter.schemas.ts` |
| `CommonCurrency` | type | `formatter.types.ts` |
| `CurrencyDisplay` | type | `formatter.types.ts` |
| `CurrencyFormatterOptions` | type | `formatter.types.ts` |
| `CurrencyInfo` | type | `formatter.types.ts` |
| `CurrencyFormatterCacheKey` | type | `formatter.types.ts` |
| `getCurrencySymbol` | function | `helpers.ts` |
| `getCurrencyName` | function | `helpers.ts` |
| `getCurrencyInfo` | function | `helpers.ts` |
| `recordCurrencyFormat` | function | `observability.ts` |
| `recordCurrencyCacheHit` | function | `observability.ts` |
| `getCurrencyFormatMetricsState` | function | `observability.ts` |
| `resetCurrencyFormatMetricsState` | function | `observability.ts` |
## Versioning
This package is part of the i18n modular-split rollout. Stays at
`0.1.0` until the slim core (Wave 6) cuts the family `1.0.0` release.
## Wave 3 Scope
This package is one of eight Wave 3 formatter packages and the only
Wave 3 formatter with an intra-formatter dependency. Pure addition —
`packages/i18n/i18n/src/formatting/currency/` is preserved in the
monolith until Wave 6 deletes it.

View File

@@ -0,0 +1,103 @@
# CurrencyFormatter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Locale-aware currency formatting backed by `Intl.NumberFormat({ style: "currency" })`. The package exposes both a class (`CurrencyFormatter` — construct once with stable currency + options, reuse) and a standalone helper (`formatCurrency(amount, currency, locale, options?)`). Every call validates the currency code through `CurrencyCodeSchema` (Zod) at the boundary (rejecting bogus codes like `"$"`, `"abc"`, `"USDT"` with a structured `INVALID_FORMATTER_OPTIONS` rather than letting them surface as raw `RangeError` from `Intl.NumberFormat`), validates options through `CurrencyFormatterOptionsSchema`, and emits a wall-clock duration sample on the `i18n.format.currency.duration_ms` histogram tagged with `locale`, `currency`, and `display`.
This package is the only Wave 3 formatter that depends on a sibling formatter — it declares `@suaveplan/i18n-formatter-number workspace:^` because currency formatting is `Intl.NumberFormat` with `style: "currency"`. The dep is documentation-grade today (the implementation constructs its own `Intl.NumberFormat`) but pins the architectural relationship the slim core (Wave 6) will exercise when wiring the default `FormatBridge`.
A per-`(locale, currency, options-hash)` cache for `Intl.NumberFormat` instances amortises construction cost across many price renders. Cache reuse increments the `i18n.format.cache_hit{formatter="currency"}` counter.
## Features
- Class form (`CurrencyFormatter`) for fixed-currency reuse + standalone helper (`formatCurrency`) for one-off calls
- `formatCurrencyValue` returns the numeric portion only (no symbol) — useful for custom rendering layouts
- BCP-47 locale resolution via the shared `LOCALE_BCP47_MAP` (re-exported by `@suaveplan/i18n-shared`)
- Display modes: `"symbol"` (default), `"code"`, `"name"`, `"narrowSymbol"`
- ISO 4217 validation enforced by Zod (`/^[A-Z]{3}$/`)
- `minimumFractionDigits` / `maximumFractionDigits` Zod-validated; inverted pair rejected as `field === 'fractionDigits'`
- Stable error codes: `I18N.INVALID_FORMATTER_OPTIONS`, `I18N.UNSUPPORTED_LOCALE`
- Process-wide cache; `clearCurrencyFormatterCache()` for tests
- OTel instrumentation: `i18n.format.currency.duration_ms` histogram, `i18n.format.cache_hit{formatter="currency"}` counter
## Usage: Basic Example
```typescript
import {
CurrencyFormatter,
formatCurrency,
formatCurrencyValue,
} from "@suaveplan/i18n-formatter-currency";
formatCurrency(1234.56, "USD", "en");
// → "$1,234.56"
formatCurrency(1234.56, "EUR", "de");
// → "1.234,56 €"
formatCurrency(100, "CHF", "en", { display: "code" });
// → "CHF 100.00"
formatCurrencyValue(1234.56, "USD", "en");
// → "1,234.56"
```
## Usage: Advanced Example — Class Form for Repeated Use
```typescript
import { CurrencyFormatter } from "@suaveplan/i18n-formatter-currency";
const usd = new CurrencyFormatter("USD", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
// Many call sites, single options + currency validation:
const lines = lineItems.map((item) => ({
label: item.label,
amount: usd.format(item.price, "en-US"),
}));
```
## API Reference
### `formatCurrency(amount, currency, locale, options?): string`
Formats `amount` per `currency`, `locale`, and `options`. Validates currency code through Zod (must match `/^[A-Z]{3}$/`), validates options through Zod, throws `I18nError(I18N.INVALID_FORMATTER_OPTIONS)` on either failure. Maps `Intl.NumberFormat`'s `RangeError` for unsupported locales / currencies to `I18nError(I18N.UNSUPPORTED_LOCALE)`.
### `formatCurrencyValue(amount, currency, locale): string`
Returns the numeric portion of the formatted currency string with the currency symbol and literal whitespace separators stripped via `formatToParts`. Reuses the same per-(locale, currency, options-hash) cache as `formatCurrency`.
### `class CurrencyFormatter`
- `constructor(currency: string, options?: CurrencyFormatterOptions)` — validates currency code + options at construction
- `format(amount: number, locale: string): string` — formats with stored currency + options
### `clearCurrencyFormatterCache(): void`
Test-only.
### `toBcp47(locale): string`
Internal BCP-47 normaliser exposed for re-use; consults `LOCALE_BCP47_MAP` from `@suaveplan/i18n-shared` for known short codes (`"en"` → `"en-GB"`), tries the lowercase fallback, then passes through unchanged.
### `CurrencyCodeSchema` and `CurrencyFormatterOptionsSchema`
Zod schemas validating the ISO 4217 code and options surface respectively.
### Types
`CommonCurrency`, `CurrencyDisplay`, `CurrencyFormatterOptions`, `CurrencyInfo`, `CurrencyFormatterCacheKey`.
## Implementation Notes
The cache key is `${locale}|${currency}|${JSON.stringify(options)}`. Including the currency in the key (rather than rolling it into the options bag) keeps `formatCurrencyValue` cache-coherent with `formatCurrency` for the same `(locale, currency)` pair under default options — both call sites resolve to the same `Intl.NumberFormat` instance.
`formatCurrencyValue` filters `formatToParts` for non-`"currency"` and non-`"literal"` parts. This correctly handles locales that place the symbol after the number (e.g. some European locales) without regex hacks. The `.trim()` at the end normalises any leading/trailing whitespace produced by edge-case locales.
ISO 4217 validation is intentionally strict — `/^[A-Z]{3}$/` rejects every code that is not exactly three uppercase letters. Cryptocurrencies like `BTC` (3 letters, all upper) pass the regex but fail at `Intl.NumberFormat` level if the runtime ICU does not recognise them; both failure modes funnel into structured `I18nError` codes (`INVALID_FORMATTER_OPTIONS` for the regex case, `UNSUPPORTED_LOCALE` for the ICU case). For non-ISO-4217 cryptocurrencies the dedicated `@suaveplan/cryptocurrency` package owns formatting.

View File

@@ -0,0 +1,74 @@
# Currency Helper Functions
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Currency-info helpers exposing structured metadata about ISO 4217 codes via `Intl.NumberFormat.formatToParts`. Use these when you need just the symbol, the localised name, or the full record (`{ code, symbol, name, decimalDigits }`) — without rendering an actual amount.
Each helper validates the currency code through the same `CurrencyCodeSchema` (Zod) used by the main `formatCurrency` standalone, so bogus codes (`"$"`, `"abc"`, `"USDT"`) raise structured `I18nError(INVALID_FORMATTER_OPTIONS)` rather than surfacing as raw `RangeError` from the underlying `Intl.NumberFormat`. Locale resolution goes through `toBcp47` from `formatter.ts` so callers can pass either a short code (`"en"` → `"en-GB"`) or a full BCP-47 tag.
## Features
- `getCurrencySymbol(currency, locale, display?)` — symbol / narrow symbol / code lookup
- `getCurrencyName(currency, locale)` — localised display name
- `getCurrencyInfo(currency, locale)` — aggregate `{ code, symbol, name, decimalDigits }` record
- ISO 4217 validation enforced before any `Intl.NumberFormat` construction
- Locale resolution via `LOCALE_BCP47_MAP` from `@suaveplan/i18n-shared`
## Usage: Basic Example
```typescript
import {
getCurrencyInfo,
getCurrencyName,
getCurrencySymbol,
} from "@suaveplan/i18n-formatter-currency/helpers";
getCurrencySymbol("GBP", "en"); // → "£"
getCurrencySymbol("USD", "en", "code"); // → "USD"
getCurrencyName("USD", "fr"); // → "dollar des États-Unis"
getCurrencyInfo("EUR", "en-US");
// → { code: "EUR", symbol: "€", name: "Euro", decimalDigits: 2 }
```
## Usage: Advanced Example — Currency picker
```typescript
import { getCurrencyInfo } from "@suaveplan/i18n-formatter-currency/helpers";
const supported = ["USD", "EUR", "GBP", "JPY", "CHF"];
function buildCurrencyOptions(locale) {
return supported.map((code) => {
const info = getCurrencyInfo(code, locale);
return {
value: info.code,
label: `${info.symbol} ${info.name} (${info.code})`,
decimals: info.decimalDigits,
};
});
}
```
## API Reference
### `getCurrencySymbol(currency, locale, display?)`
Returns the currency symbol string. The `display` argument restricts to `"symbol"` (default), `"narrowSymbol"`, or `"code"` — the same surface as the corresponding `Intl.NumberFormat` option restricted to the symbol-shaped subset. Throws `I18nError(INVALID_FORMATTER_OPTIONS)` if the currency code does not match `/^[A-Z]{3}$/`.
### `getCurrencyName(currency, locale)`
Returns the localised display name (e.g. `"US dollar"` in `en`, `"dollar américain"` in `fr`). Throws `I18nError(INVALID_FORMATTER_OPTIONS)` for malformed currency codes.
### `getCurrencyInfo(currency, locale)`
Returns an aggregate `CurrencyInfo` record containing the code, symbol, localised name, and decimal-digit count. Decimal digits are resolved from `formatter.resolvedOptions().maximumFractionDigits`, ensuring JPY returns 0, BHD returns 3, and most other currencies return 2 automatically.
## Implementation Notes
The helpers do NOT participate in the `formatCurrency` cache because they ask `Intl.NumberFormat` for a different option set per call (`currencyDisplay: "code"` for symbol-as-code, `currencyDisplay: "name"` for the name). Sharing a cache would require keying on those options, which would defeat the simpler `(locale, currency, options-hash)` shape used by the main formatter.
The `formatToParts(0)` / `formatToParts(1)` calls use a small constant amount because some locales render different symbols at different magnitudes (rare but possible in less-common ICU builds). Using `0` for symbol lookup and `1` for name lookup matches the monolith's behaviour for cross-package compatibility.

View File

@@ -0,0 +1,91 @@
# CurrencyFormatter Observability
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Per-package OpenTelemetry instruments for `@suaveplan/i18n-formatter-currency`. Exposes a histogram (`i18n.format.currency.duration_ms`) recording wall-clock duration of every `formatCurrency` call (tagged with `locale`, `currency`, and `display`) and a counter (`i18n.format.cache_hit`) tagged with `formatter: "currency"` that increments on every cache reuse.
The currency formatter introduces these instruments as part of the Wave 3 uplift — the monolith currently emits no OTel for currency formatting. The two instruments are constructed once via `metrics.getMeter` and are lazily initialised on first record so cold-start latency stays low.
## Features
- Lazy instrument construction
- Stable histogram name `i18n.format.currency.duration_ms` with attributes `{ locale, currency, display }`
- Stable counter name `i18n.format.cache_hit` with attributes `{ formatter: "currency" }`
- In-memory sample ring (`currencyDurationSamples`, `cacheHitSamples`)
- `resetCurrencyFormatMetricsState()` clears both arrays so tests stay isolated
## Usage: Basic Example
```typescript
import {
recordCurrencyFormat,
recordCurrencyCacheHit,
getCurrencyFormatMetricsState,
resetCurrencyFormatMetricsState,
} from "@suaveplan/i18n-formatter-currency/observability";
resetCurrencyFormatMetricsState();
recordCurrencyFormat("en-US", "USD", "symbol", 0.4);
recordCurrencyCacheHit();
const state = getCurrencyFormatMetricsState();
state.currencyDurationSamples.length; // 1
state.cacheHitSamples.length; // 1
```
## Usage: Advanced Example — Asserting OTel emission in a test
```typescript
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
formatCurrency,
clearCurrencyFormatterCache,
} from "@suaveplan/i18n-formatter-currency";
import {
getCurrencyFormatMetricsState,
resetCurrencyFormatMetricsState,
} from "@suaveplan/i18n-formatter-currency/observability";
describe("metrics emission", () => {
it("emits duration histogram and cache-hit counter", { timeout: 5000 }, () => {
clearCurrencyFormatterCache();
resetCurrencyFormatMetricsState();
formatCurrency(42, "USD", "en-US");
formatCurrency(42, "USD", "en-US");
const state = getCurrencyFormatMetricsState();
expect(state.currencyDurationSamples).toHaveLength(2);
expect(state.cacheHitSamples).toHaveLength(1);
});
});
```
## API Reference
### `recordCurrencyFormat(locale, currency, display, durationMs): void`
Records one observation on the `i18n.format.currency.duration_ms` histogram and pushes a `{ attributes: { locale, currency, display } }` entry into the in-memory sample ring.
### `recordCurrencyCacheHit(): void`
Adds 1 to the `i18n.format.cache_hit` counter with `formatter: "currency"` and pushes a matching entry into the in-memory sample ring.
### `getCurrencyFormatMetricsState(): CurrencyFormatMetricsState`
Returns the in-memory sample ring (live reference — do not mutate).
### `resetCurrencyFormatMetricsState(): void`
Empties both sample arrays in place.
### `CurrencyFormatMetricsState`
`{ currencyDurationSamples: FormatMetricSample[]; cacheHitSamples: FormatMetricSample[] }` where `FormatMetricSample = { attributes: Record<string, string | number> }`.
## Implementation Notes
The instruments are built lazily so consumers who never call `formatCurrency` do not pay the OTel meter-lookup cost. The cache check is a strict identity check against the closure-scoped `cached` slot — once built, the instruments are never rebuilt, even across `resetCurrencyFormatMetricsState` calls.
The `currency` attribute is the ISO 4217 code (`"USD"`, `"EUR"`, `"JPY"`); the `display` attribute is the resolved display mode (`"symbol"`, `"code"`, `"name"`, `"narrowSymbol"`). Both are included in the histogram tags so consumers can dashboard the most-formatted currencies and the most-used display modes per locale.