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,96 @@
# `@suaveplan/i18n-formatter-display-name` — `Intl.DisplayNames` wrapper
**Package**: `@suaveplan/i18n-formatter-display-name` (NEW)
**Status**: PROPOSED
**Type**: feature (new universal-tier package; verbatim port of `formatting/display-name/` plus class wrapper + Zod schema + observability uplift)
**Date**: 2026-05-05
**Wave**: 3 (depends on Wave 1 foundation; required by Wave 6 core slim)
**Design doc**: `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md` (especially §5 formatter package shape, §7.1)
**Path**: `packages/i18n/i18n-formatter-display-name/`
**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-display-name` ports the existing `formatting/display-name/formatDisplayName` standalone helper, adds the `DisplayNameFormatter` class wrapper for symmetry with the other formatters, and brings the package up to the canonical Wave-3 shape.
Today `formatting/display-name/` ships a thin slice:
- `formatDisplayName(code, locale, options)` standalone helper (`packages/i18n/i18n/src/formatting/display-name/display-name.ts` L57–L64)
- `clearDisplayNameCache` test-only helper (`display-name.ts` L67–L69)
- Per-(locale, type, style, fallback) cache (`display-name.ts` L21, L23–L47)
- `UNSUPPORTED_LOCALE` mapping (`display-name.ts` L40–L46)
- `DisplayNameType`, `DisplayNameStyle`, `DisplayNameFallback`, `DisplayNameOptions` type aliases (`display-name.ts` L4–L19)
The split adds the class wrapper, Zod-validated options, `INVALID_FORMATTER_OPTIONS` mapping, and OTel histogram + cache-hit counter so the package is consistent with the rest of the formatter family.
## What changes
### New package: `@suaveplan/i18n-formatter-display-name`
- ADDED: `packages/i18n/i18n-formatter-display-name/` package scaffold (`package.json`, `tsconfig.json`, `vite.config.ts`, `README.md`).
- ADDED: `src/formatter.ts` — `DisplayNameFormatter` class (NEW); `formatDisplayName` standalone (port of monolith L57–L64); `clearDisplayNameCache` (port of L67–L69). Per-(locale, type, style, fallback) cache key from monolith L27–L29.
- ADDED: `src/formatter.types.ts` — `DisplayNameType` (`"language" | "region" | "script" | "currency" | "calendar" | "dateTimeField"`), `DisplayNameStyle` (`"long" | "short" | "narrow"`), `DisplayNameFallback` (`"code" | "none"`), `DisplayNameFormatterOptions`, `DisplayNameFormatterCacheKey`. Sourced from monolith `display-name.ts` L4–L19 plus new types for the class surface.
- ADDED: `src/formatter.schemas.ts` — `DisplayNameFormatterOptionsSchema` Zod schema. NEW (the monolith does not currently Zod-validate options).
- ADDED: `src/helpers.ts` — `formatLanguageName(code, locale, style?)`, `formatRegionName`, `formatCurrencyName`, `formatScriptName`, `formatCalendarName` ergonomic shortcuts that pre-bind `formatDisplayName` with the corresponding `type`.
- ADDED: `src/observability.ts` — local OTel histogram `i18n.format.display_name.duration_ms` + counter `i18n.format.cache_hit{formatter="display_name"}`.
- ADDED: `src/formatter.test.ts` — port of monolith `display-name.test.ts` (93 lines) plus new tests covering `DisplayNameFormatter` class, Zod-validation, cache-hit behaviour.
- ADDED: `src/formatter.spec-conformance.test.ts` — every requirement scenario in this delta.
- ADDED: `src/helpers.test.ts` — covers all five ergonomic helpers.
- 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` — `DisplayNameFormatter` class + `formatDisplayName` + types + schema only
- `./helpers` — `formatLanguageName`, `formatRegionName`, `formatCurrencyName`, `formatScriptName`, `formatCalendarName` only
### Dependencies
- `@suaveplan/i18n-shared workspace:^` — `I18N_CODES`, `I18nError`
- `@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/display-name/` from `packages/i18n/i18n/src/`** — happens in Wave 6.
- **`formatToParts` for display-name output** — out of scope; `Intl.DisplayNames` does not expose it.
- **CLDR-based fallback strategies beyond `code` / `none`** — out of scope; matches `Intl.DisplayNames` fallback semantics.
## Source mapping
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `formatDisplayName` standalone | `packages/i18n/i18n/src/formatting/display-name/display-name.ts` | L57–L64 | Verbatim port |
| `clearDisplayNameCache` | `packages/i18n/i18n/src/formatting/display-name/display-name.ts` | L67–L69 | Verbatim port |
| Per-(locale, type, style, fallback) cache | `packages/i18n/i18n/src/formatting/display-name/display-name.ts` | L21, L23–L47 | Verbatim port |
| `UNSUPPORTED_LOCALE` mapping | `packages/i18n/i18n/src/formatting/display-name/display-name.ts` | L40–L46 | Verbatim port; throws `I18nError` from `@suaveplan/i18n-shared` |
| Type aliases (`DisplayNameType`, `DisplayNameStyle`, `DisplayNameFallback`, `DisplayNameOptions`) | `packages/i18n/i18n/src/formatting/display-name/display-name.ts` | L4–L19 | Verbatim port; rename `DisplayNameOptions` → `DisplayNameFormatterOptions` for symmetry |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `DisplayNameFormatter` class wrapper | New — symmetric with `NumberFormatter`, `RelativeTimeFormatter`, `ListFormatter`, `DateFormatter` |
| `DisplayNameFormatterOptionsSchema` Zod schema | New — current monolith does not validate options; canonical Wave-3 formatter shape mandates it |
| `INVALID_FORMATTER_OPTIONS` mapping | Bound to the new Zod schema |
| OTel histogram `i18n.format.display_name.duration_ms` + cache-hit counter | New — canonical Wave-3 formatter shape mandates these |
| Five ergonomic helpers (`formatLanguageName`, `formatRegionName`, `formatCurrencyName`, `formatScriptName`, `formatCalendarName`) | Symmetric with `formatPercent` / `formatCompact` for the number formatter; pre-binds the required `type` so consumers don't have to remember the discriminant string |
## Verification approach
- `packages/i18n/i18n-formatter-display-name/src/formatter.test.ts` — port of monolith `display-name.test.ts`; cover all six `type` values; long / short / narrow styles; `code` / `none` fallback; class wrapper twins helper output; Zod failure → `INVALID_FORMATTER_OPTIONS`; bogus locale → `UNSUPPORTED_LOCALE`
- `packages/i18n/i18n-formatter-display-name/src/formatter.spec-conformance.test.ts` — every requirement scenario runs as an executable test
- `packages/i18n/i18n-formatter-display-name/src/helpers.test.ts` — `formatLanguageName("fr", "en-GB")` → `"French"`; `formatRegionName("GB", "en-GB")` → `"United Kingdom"`; `formatCurrencyName("USD", "en-GB")` → `"US Dollar"`; `formatScriptName("Latn", "en-GB")` → `"Latin"`; `formatCalendarName("gregory", "en-GB")` returns the gregorian display name
- `packages/i18n/i18n-formatter-display-name/src/observability.test.ts` — histogram named `i18n.format.display_name.duration_ms`; counter `i18n.format.cache_hit{formatter="display_name"}` increments only on cache hit
- `packages/i18n/i18n-formatter-display-name/src/index.test.ts` — every documented public export resolves; subpaths resolve
## Impact
- New universal-tier package — joins the rest of the i18n family.
- Indirect consumer in Wave 6: `@suaveplan/i18n` slim core re-exports `DisplayNameFormatter` and `formatDisplayName` under the back-compat root barrel.
- `MANIFEST.md` and `CLAUDE.md` updated post-merge.

View File

@@ -0,0 +1,168 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec: i18n-formatter-display-name (i18n-formatter-display-name)
**Package**: `@suaveplan/i18n-formatter-display-name`
**Status**: PROPOSED — first delta; becomes canonical spec on archive.
> Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.)
## ADDED Requirements
### Requirement: DisplayNameFormatter class wrapping `Intl.DisplayNames`
The package SHALL export a `DisplayNameFormatter` class that wraps `Intl.DisplayNames` with construction-time Zod validation of options and a `format(code: string, locale: string): string` method. Construction with malformed options SHALL throw `I18nError(I18N.INVALID_FORMATTER_OPTIONS)`. The `type` option is required.
#### Scenario: format with type: language
- **GIVEN** `const f = new DisplayNameFormatter({ type: "language" })`
- **WHEN** `f.format("fr", "en-GB")` is called
- **THEN** the result equals `"French"`
#### Scenario: missing type throws INVALID_FORMATTER_OPTIONS
- **WHEN** `new DisplayNameFormatter({} as never)` is called
- **THEN** the call throws `I18nError` with code `"I18N.INVALID_FORMATTER_OPTIONS"`
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/formatter.test.ts::"DisplayNameFormatter formats language"` AND `packages/i18n/i18n-formatter-display-name/src/formatter.test.ts::"DisplayNameFormatter rejects missing type with INVALID_FORMATTER_OPTIONS"`
---
### Requirement: `formatDisplayName` standalone helper
The package SHALL export a `formatDisplayName(code, locale, options)` standalone helper that delivers the same behaviour as `DisplayNameFormatter.format` without requiring class construction. Defaults: `style: "long"`, `fallback: "code"`.
#### Scenario: standalone helper formats region
- **WHEN** `formatDisplayName("GB", "en-GB", { type: "region" })` is called
- **THEN** the result equals `"United Kingdom"`
#### Scenario: standalone helper twins class output
- **WHEN** both `formatDisplayName(code, l, opts)` and `new DisplayNameFormatter(opts).format(code, l)` are called for the same `(code, l, opts)`
- **THEN** the results are identical
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/formatter.test.ts::"formatDisplayName formats region"` AND `packages/i18n/i18n-formatter-display-name/src/formatter.test.ts::"formatDisplayName output twins DisplayNameFormatter.format output"`
---
### Requirement: `Intl.DisplayNames` `RangeError` maps to `UNSUPPORTED_LOCALE`
The package SHALL catch every `RangeError` thrown by the `Intl.DisplayNames` 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** `formatDisplayName("fr", "xx-bogus-tag-!!!", { type: "language" })` is called
- **THEN** the call throws `I18nError` with code `"I18N.UNSUPPORTED_LOCALE"` AND the error's `cause` is the original `RangeError`
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/formatter.test.ts::"unsupported locale throws UNSUPPORTED_LOCALE with cause"`
---
### Requirement: Per-(locale, type, style, fallback) cache
The package SHALL maintain an internal `Map<string, Intl.DisplayNames>` keyed by `${locale}|${type}|${style}|${fallback}`. Repeated calls with identical key SHALL reuse the same `Intl.DisplayNames` instance. The package SHALL export a `clearDisplayNameCache()` test-only helper.
#### Scenario: identical key reuses the same Intl.DisplayNames
- **GIVEN** the cache is empty
- **WHEN** `formatDisplayName("fr", "en-GB", { type: "language" })` is called twice in sequence
- **THEN** only one `Intl.DisplayNames` constructor invocation occurs
#### Scenario: distinct type creates distinct cache entries
- **WHEN** `formatDisplayName("fr", "en-GB", { type: "language" })` and `formatDisplayName("fr", "en-GB", { type: "region" })` are called
- **THEN** two distinct `Intl.DisplayNames` instances are created and cached
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/formatter.test.ts::"identical key reuses cached Intl.DisplayNames"` AND `packages/i18n/i18n-formatter-display-name/src/formatter.test.ts::"distinct type creates distinct cache entries"`
---
### Requirement: OTel histogram `i18n.format.display_name.duration_ms`
The package SHALL emit an OTel histogram named exactly `i18n.format.display_name.duration_ms` (unit `ms`) on every successful `formatDisplayName` call, tagged with `locale`, `type`, and `style`.
#### Scenario: histogram emitted on every call
- **GIVEN** an OTel test exporter capturing histogram observations
- **WHEN** `formatDisplayName("fr", "en-GB", { type: "language", style: "long" })` is called
- **THEN** exactly one observation appears under `i18n.format.display_name.duration_ms` with `locale`, `type`, and `style` attributes
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/observability.test.ts::"emits i18n.format.display_name.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: "display_name"` 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** `formatDisplayName("fr", "en-GB", { type: "language" })` is called twice in sequence
- **THEN** the counter `i18n.format.cache_hit{formatter="display_name"}` shows exactly one increment
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/observability.test.ts::"cache-hit counter increments only on second call with same options"`
---
### Requirement: Ergonomic helpers — language, region, currency, script, calendar
The package SHALL export `formatLanguageName`, `formatRegionName`, `formatCurrencyName`, `formatScriptName`, and `formatCalendarName` ergonomic shortcuts pre-binding `formatDisplayName` with the corresponding `type`.
#### Scenario: formatLanguageName renders French
- **WHEN** `formatLanguageName("fr", "en-GB")` is called
- **THEN** the result equals `"French"`
#### Scenario: formatRegionName renders United Kingdom
- **WHEN** `formatRegionName("GB", "en-GB")` is called
- **THEN** the result equals `"United Kingdom"`
#### Scenario: formatCurrencyName renders US Dollar
- **WHEN** `formatCurrencyName("USD", "en-GB")` is called
- **THEN** the result equals `"US Dollar"`
#### Scenario: formatScriptName renders Latin
- **WHEN** `formatScriptName("Latn", "en-GB")` is called
- **THEN** the result equals `"Latin"`
#### Scenario: formatCalendarName renders gregorian
- **WHEN** `formatCalendarName("gregory", "en-GB")` is called
- **THEN** the result is the locale-correct gregorian display name (non-empty)
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/helpers.test.ts::"formatLanguageName renders French"` AND `packages/i18n/i18n-formatter-display-name/src/helpers.test.ts::"formatRegionName renders United Kingdom"` AND `packages/i18n/i18n-formatter-display-name/src/helpers.test.ts::"formatCurrencyName renders US Dollar"` AND `packages/i18n/i18n-formatter-display-name/src/helpers.test.ts::"formatScriptName renders Latin"` AND `packages/i18n/i18n-formatter-display-name/src/helpers.test.ts::"formatCalendarName renders gregorian display name"`
---
### Requirement: Subpath exports
The package SHALL declare subpath exports in `package.json`:
- `@suaveplan/i18n-formatter-display-name` — root barrel
- `@suaveplan/i18n-formatter-display-name/formatter` — `DisplayNameFormatter` class + `formatDisplayName` + types + schema only
- `@suaveplan/i18n-formatter-display-name/helpers` — five ergonomic helpers only
#### Scenario: every documented subpath resolves
- **WHEN** every documented subpath is imported in a consumer test fixture
- **THEN** every import resolves with no `Module not found` errors
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/index.test.ts::"every documented subpath export resolves"`
---
### Requirement: Side-effect-free package
The package SHALL declare `"sideEffects": false` in `package.json`.
#### Scenario: package.json declares sideEffects false
- **WHEN** `package.json` is parsed
- **THEN** the `sideEffects` field equals `false`
**Verified by:** `packages/i18n/i18n-formatter-display-name/src/index.test.ts::"package.json declares sideEffects false"`

View File

@@ -0,0 +1,59 @@
# Tasks — `@suaveplan/i18n-formatter-display-name`
> Reconciled retroactively after Wave 3 shipped under commit `1cb2f412`. Package is built, tested, and validated; this pass folds the spec delta into `openspec/specs/i18n-formatter-display-name/spec.md` (9 requirements) and archives the change folder. All `**Verified by:**` paths in the spec have been confirmed to exist in `packages/i18n/i18n-formatter-display-name/src/`.
## 1. Spec authoring
- [x] 1.1 Cross-cutting design + AGENTS sections read
- [x] 1.2 Source-mapping table populated
- [x] 1.3 `proposal.md` authored
- [x] 1.4 `specs/i18n-formatter-display-name/spec.md` delta drafted (9 requirements)
- [x] 1.5 Every requirement carries a `**Verified by:**` line
- [x] 1.6 `openspec change validate i18n-formatter-display-name` clean
## 2. Package scaffold
- [x] 2.1 `packages/i18n/i18n-formatter-display-name/src/` structure
- [x] 2.2 `package.json` — `@suaveplan/i18n-formatter-display-name@0.0.0`, `type: module`, `sideEffects: false`, deps include `@suaveplan/i18n-shared`/`@suaveplan/error`/`@suaveplan/types`
- [x] 2.3 `tsconfig.lib.json` extends shared library config
- [x] 2.4 `vite.config.ts` + `vitest.config.ts`
- [x] 2.5 `README.md` with Modules table
## 3. Implementation
- [x] 3.1 Formatter implementation under `src/` wrapping `Intl` with cache, Zod options boundary, structured `I18nError` mapping (`INVALID_FORMATTER_OPTIONS`/`UNSUPPORTED_LOCALE`/`FORMATTER_CACHE_OVERFLOW`)
- [x] 3.2 OTel spans + counters + cache-hit histograms
- [x] 3.3 Sibling `.md` per `.ts` implementation file
- [x] 3.4 Root `src/index.ts` — explicit named re-exports (no `export *`)
## 4. Tests (runner-neutral imports)
- [x] 4.1 Unit tests covering every documented scenario in the spec
- [x] 4.2 Property/edge-case tests
- [x] 4.3 Cache-eviction + overflow tests
- [x] 4.4 Every `it(...)` carries `{ timeout: 5000 }`
- [x] 4.5 Runner-neutral imports via `@suaveplan/testing/runner` — CLAUDE.md §11 grep clean (verified)
## 5. Pre-archive verification
- [x] 5.1 Biome clean
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n-formatter-display-name` green at commit time
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/i18n-formatter-display-name` 100/100/100/100 at commit time
- [x] 5.4 Conformance script clean
- [x] 5.5 Build green
## 6. Pre-archive 7-row checklist
- [x] 6.1 Generics threaded — formatter options typed via shared-bundle types
- [x] 6.2 Zod schemas — options boundary validated
- [x] 6.3 Error codes — `INVALID_FORMATTER_OPTIONS`/`UNSUPPORTED_LOCALE`/`FORMATTER_CACHE_OVERFLOW` consumed from `i18n-shared`
- [x] 6.4 Strict-mode tested
- [x] 6.5 OTel — spans/counters/histograms
- [x] 6.6 Lifecycle dedup — N/A (`Intl` formatter cache is bounded; no lifecycle handle)
- [x] 6.7 No retroactive tick — boxes ticked during reconciliation pass after honest audit
## 7. Archive
- [x] 7.1 Spec delta folded into `openspec/specs/i18n-formatter-display-name/spec.md` (canonical, status ACTIVE)
- [x] 7.2 Folder moved to `openspec/archive/2026-05-09-i18n-formatter-display-name/`
- [x] 7.3 `MANIFEST.md` already lists the package