Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
# @suaveplan/i18n-formatter-display-name - Intl.DisplayNames wrapper for the @suaveplan/i18n family — locale-aware language/region/script/currency/calendar display names. Per-(locale, type, style, fallback) cache, Zod-validated options, OTel histogram + cache-hit counter, structured I18nError mapping. Universal leaf package.
## Summary
Wrapper around `Intl.DisplayNames` for the `@suaveplan/i18n` family —
locale-aware language, region, script, currency, and calendar display
names. Universal leaf package; depends only on
`@suaveplan/i18n-shared`, `@suaveplan/types`, `@suaveplan/error`,
`@opentelemetry/api`, and `zod`.
<!-- begin:auto-docs -->
## Documentation tree
- [formatter](./src/formatter/formatter.md) — `DisplayNameFormatter` and the `formatDisplayName` standalone helper expose `Intl.DisplayNames` for the `@suaveplan/i18…
- [formatter](./src/formatter/formatter.md) — `DisplayNameFormatter` and the `formatDisplayName` standalone helper expose `Intl.DisplayNames` for the `@suaveplan/i18…
- [helpers](./src/helpers/helpers.md) — The five ergonomic helpers (`formatLanguageName`, `formatRegionName`, `formatCurrencyName`, `formatScriptName`, `format…
- [helpers](./src/helpers/helpers.md) — The five ergonomic helpers (`formatLanguageName`, `formatRegionName`, `formatCurrencyName`, `formatScriptName`, `format…
- [observability](./src/observability/observability.md) — The DisplayName formatter exports two OpenTelemetry instruments and a small in-memory state ring so spec-conformance te…
- [observability](./src/observability/observability.md) — The DisplayName formatter exports two OpenTelemetry instruments and a small in-memory state ring so spec-conformance te…
<!-- end:auto-docs -->
## Why this exists
The 14-package i18n modular split (cross-cutting design
`2026-05-05-i18n-modular-split.md`) lifts each `Intl.*` wrapper out of
the monolith into its own sibling package. Lifting the display-name
helper out of `@suaveplan/i18n` lets consumers that only need
country-name / currency-name rendering pull a single small package
instead of the entire i18n core.
The package adds the canonical Wave-3 formatter shape on top of the
monolith's standalone helper: Zod-validated options, a per-(locale,
type, style, fallback) cache, OTel histogram + cache-hit counter, the
class wrapper for symmetry with `NumberFormatter` /
`RelativeTimeFormatter` / `ListFormatter` / `DateFormatter`, and five
ergonomic helpers that pre-bind `formatDisplayName` with each
canonical `type` value.
## Install
```bash
bun add @suaveplan/i18n-formatter-display-name
```
## Quick Start
```ts
import {
DisplayNameFormatter,
formatDisplayName,
formatLanguageName,
formatRegionName,
formatCurrencyName,
} from "@suaveplan/i18n-formatter-display-name";
formatDisplayName("GB", "en-GB", { type: "region" }); // "United Kingdom"
formatLanguageName("fr", "en-GB"); // "French"
formatRegionName("US", "en-GB"); // "United States"
formatCurrencyName("USD", "en-GB"); // "US Dollar"
const f = new DisplayNameFormatter({ type: "region", style: "short" });
f.format("DE", "en-GB"); // "Germany"
```
## Modules
| Subpath | Description |
|---|---|
| `@suaveplan/i18n-formatter-display-name` | Root barrel — class + standalones + helpers + observability |
| `@suaveplan/i18n-formatter-display-name/formatter` | `DisplayNameFormatter`, `formatDisplayName`, types, schema |
| `@suaveplan/i18n-formatter-display-name/helpers` | Five ergonomic helpers — `formatLanguageName`, `formatRegionName`, `formatCurrencyName`, `formatScriptName`, `formatCalendarName` |
## Modules Documentation
- [formatter.md](src/formatter.md) — `DisplayNameFormatter` class, `formatDisplayName`, `clearDisplayNameCache`
- [helpers.md](src/helpers.md) — five ergonomic helpers
- [observability.md](src/observability.md) — OTel histogram + cache-hit counter
## Public Surface (snapshot)
| Export | Kind | Origin |
|---|---|---|
| `DisplayNameFormatter` | class | `formatter.ts` |
| `formatDisplayName` | function | `formatter.ts` |
| `clearDisplayNameCache` | function | `formatter.ts` |
| `DisplayNameFormatterOptionsSchema` | Zod schema | `formatter.schemas.ts` |
| `DisplayNameFormatterOptions` | type | `formatter.types.ts` |
| `DisplayNameType` | type | `formatter.types.ts` |
| `DisplayNameStyle` | type | `formatter.types.ts` |
| `DisplayNameFallback` | type | `formatter.types.ts` |
| `DisplayNameFormatterCacheKey` | type | `formatter.types.ts` |
| `formatLanguageName` | function | `helpers.ts` |
| `formatRegionName` | function | `helpers.ts` |
| `formatCurrencyName` | function | `helpers.ts` |
| `formatScriptName` | function | `helpers.ts` |
| `formatCalendarName` | function | `helpers.ts` |
| `recordDisplayNameFormat` | function | `observability.ts` |
| `recordDisplayNameCacheHit` | function | `observability.ts` |
| `getDisplayNameMetricsState` | function | `observability.ts` |
| `resetDisplayNameMetricsState` | function | `observability.ts` |
## Versioning
Part of the 14-package i18n modular-split rollout. Stays at `0.1.0`
until the slim core (Wave 6) cuts the family `1.0.0` release.

View File

@@ -0,0 +1,76 @@
{
"name": "@suaveplan/i18n-formatter-display-name",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Intl.DisplayNames wrapper for the @suaveplan/i18n family — locale-aware language/region/script/currency/calendar display names. Per-(locale, type, style, fallback) cache, Zod-validated options, OTel histogram + cache-hit counter, structured I18nError mapping. Universal leaf package.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./formatter": {
"import": {
"types": "./dist/formatter/index.d.ts",
"default": "./dist/formatter/index.js"
}
},
"./helpers": {
"import": {
"types": "./dist/helpers/index.d.ts",
"default": "./dist/helpers/index.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/error": "workspace:^",
"@suaveplan/i18n-shared": "workspace:^",
"@suaveplan/types": "workspace:^",
"@opentelemetry/api": "catalog:observability",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@suaveplan/typescript-config": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": [
"dist",
"src",
"README.md"
]
}

View File

@@ -0,0 +1,23 @@
import z from "zod";
/**
* Zod schema for `DisplayNameFormatterOptions`. `type` is required
* (per `Intl.DisplayNames` semantics); `style` and `fallback` are
* optional and accept the canonical enum values only.
*/
export const DisplayNameFormatterOptionsSchema = z.object({
type: z.enum([
"language",
"region",
"script",
"currency",
"calendar",
"dateTimeField",
]),
style: z.enum(["long", "short", "narrow"]).optional(),
fallback: z.enum(["code", "none"]).optional(),
});
export type DisplayNameFormatterOptionsInput = z.input<
typeof DisplayNameFormatterOptionsSchema
>;

View File

@@ -0,0 +1,138 @@
/**
* Spec-conformance pins for DisplayNameFormatter — Zod rejection
* paths, Intl.RangeError mapping, OTel emission, and cache reuse
* semantics. Asserts every requirement in the
* `i18n-formatter-display-name` capability spec runs as an executable
* test.
*/
import { I18N_CODES, I18nError } from "@suaveplan/i18n-shared";
import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner";
import {
clearDisplayNameCache,
DisplayNameFormatter,
formatDisplayName,
} from "./formatter/formatter.js";
import type {
DisplayNameFormatterOptions,
DisplayNameType,
} from "./formatter.types.js";
import {
getDisplayNameMetricsState,
resetDisplayNameMetricsState,
} from "./observability/observability.js";
describe("DisplayNameFormatter spec-conformance", () => {
it("rejects non-object options shape (no path → field='options')", {
timeout: 5000,
}, () => {
try {
// @ts-expect-error wrong type for safeParse
formatDisplayName("fr", "en-GB", "not-an-object");
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(
I18N_CODES.INVALID_FORMATTER_OPTIONS
);
}
});
it("rejects unknown type with INVALID_FORMATTER_OPTIONS", {
timeout: 5000,
}, () => {
try {
new DisplayNameFormatter({ type: "magic" as never });
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(
I18N_CODES.INVALID_FORMATTER_OPTIONS
);
expect((err as I18nError).context).toMatchObject({
field: "type",
});
}
});
it("rejects unknown style with INVALID_FORMATTER_OPTIONS", {
timeout: 5000,
}, () => {
try {
new DisplayNameFormatter({
type: "language",
style: "magic" as never,
});
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(
I18N_CODES.INVALID_FORMATTER_OPTIONS
);
expect((err as I18nError).context).toMatchObject({
field: "style",
});
}
});
it("maps Intl RangeError to UNSUPPORTED_LOCALE", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
try {
formatDisplayName("GB", "en US", { type: "region" });
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(I18N_CODES.UNSUPPORTED_LOCALE);
}
});
it("reuses cached Intl.DisplayNames on identical key", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
resetDisplayNameMetricsState();
formatDisplayName("fr", "en-GB", { type: "language" });
formatDisplayName("fr", "en-GB", { type: "language" });
const state = getDisplayNameMetricsState();
expect(state.cacheHitSamples).toHaveLength(1);
expect(state.cacheHitSamples[0]?.attributes.formatter).toBe(
"display_name"
);
});
it("does not reuse cache across distinct (type, style) keys", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
resetDisplayNameMetricsState();
formatDisplayName("fr", "en-GB", { type: "language", style: "long" });
formatDisplayName("fr", "en-GB", { type: "language", style: "short" });
const state = getDisplayNameMetricsState();
expect(state.cacheHitSamples).toHaveLength(0);
});
it("records a duration_ms sample per call", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
resetDisplayNameMetricsState();
formatDisplayName("fr", "en-GB", { type: "language", style: "long" });
const state = getDisplayNameMetricsState();
expect(state.durationSamples).toHaveLength(1);
expect(state.durationSamples[0]?.attributes).toMatchObject({
locale: "en-GB",
type: "language",
style: "long",
});
});
it("type field of options matches DisplayNameType", {
timeout: 5000,
}, () => {
expectTypeOf<
DisplayNameFormatterOptions["type"]
>().toEqualTypeOf<DisplayNameType>();
});
});

View File

@@ -0,0 +1,37 @@
/** DisplayName formatter type definitions. */
/**
* Discriminant union of every code domain `Intl.DisplayNames` can
* resolve. Sourced from monolith `formatting/display-name/display-name.ts`
* L4–L10.
*/
export type DisplayNameType =
| "language"
| "region"
| "script"
| "currency"
| "calendar"
| "dateTimeField";
/** Display style — long, short, narrow. */
export type DisplayNameStyle = "long" | "short" | "narrow";
/** Fallback policy: return code on lookup miss, or `undefined`. */
export type DisplayNameFallback = "code" | "none";
/**
* Public options shape consumed by `DisplayNameFormatter` and
* `formatDisplayName`. `type` is required; `style` defaults to
* `"long"`; `fallback` defaults to `"code"`.
*/
export interface DisplayNameFormatterOptions {
readonly type: DisplayNameType;
readonly style?: DisplayNameStyle;
readonly fallback?: DisplayNameFallback;
}
/**
* Cache key shape: `${locale}|${type}|${style}|${fallback}` where
* absent fields fall back to `"long"` / `"code"` respectively.
*/
export type DisplayNameFormatterCacheKey = string;

View File

@@ -0,0 +1,98 @@
# DisplayNameFormatter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DisplayNameFormatter` and the `formatDisplayName` standalone helper
expose `Intl.DisplayNames` for the `@suaveplan/i18n` family at the
canonical Wave-3 formatter surface: Zod-validated options, structured
error mapping, per-(locale, type, style, fallback) cache, and OTel
emission. Consumers convert ISO codes (`"GB"`, `"USD"`, `"fr"`,
`"gregory"`) into their localised display names — the universal way to
render country names, currencies, languages, scripts, and calendar
identifiers in a user's locale.
## Features
- `DisplayNameFormatter` class with stored options + `format(code, locale)` method
- `formatDisplayName(code, locale, options)` standalone helper
- All six `Intl.DisplayNames` `type` values: `language` / `region` / `script` / `currency` / `calendar` / `dateTimeField`
- Three `style` values: `long` / `short` / `narrow`
- Two `fallback` policies: `code` (default) / `none`
- Construction-time Zod validation with `INVALID_FORMATTER_OPTIONS` mapping
- `Intl.DisplayNames` `RangeError` mapped to `UNSUPPORTED_LOCALE` with original error attached as `cause`
- Per-(locale, type, style, fallback) cache — repeated calls reuse the same `Intl.DisplayNames`
- `clearDisplayNameCache()` test-only helper
## Usage: Basic Example
```ts
import { formatDisplayName } from "@suaveplan/i18n-formatter-display-name";
formatDisplayName("GB", "en-GB", { type: "region" }); // "United Kingdom"
formatDisplayName("fr", "en-GB", { type: "language" }); // "French"
formatDisplayName("USD", "en-GB", { type: "currency" }); // "US Dollar"
```
## Usage: Advanced Example
```ts
import {
DisplayNameFormatter,
type DisplayNameFormatterOptions,
} from "@suaveplan/i18n-formatter-display-name";
const opts: DisplayNameFormatterOptions = {
type: "region",
style: "short",
fallback: "none",
};
const f = new DisplayNameFormatter(opts);
// Reuses the same cached Intl.DisplayNames per locale.
const us = f.format("US", "en-GB");
const fr = f.format("FR", "fr-FR");
// Unknown region with fallback: "none" returns the code unchanged.
const unknown = f.format("ZZ", "en-GB"); // "ZZ"
```
## API Reference
### `formatDisplayName(code, locale, options): string`
Standalone helper. Validates `options` via Zod, looks up or constructs
the cached `Intl.DisplayNames`, and returns
`formatter.of(code) ?? code`. Throws `I18nError(INVALID_FORMATTER_OPTIONS)`
on Zod failure or `I18nError(UNSUPPORTED_LOCALE)` on
`Intl.DisplayNames` `RangeError`.
### `class DisplayNameFormatter`
- `constructor(options: DisplayNameFormatterOptions)` — stores options after Zod validation
- `format(code: string, locale: string): string` — same semantics as the standalone
### `clearDisplayNameCache(): void`
Test-only helper that empties the per-(locale, type, style, fallback)
cache.
## Implementation Notes
- The cache is a process-wide `Map<string, Intl.DisplayNames>` keyed
by `${locale}|${type}|${style}|${fallback}`. The single shared
cache is a deliberate choice — `Intl.DisplayNames` instances are
immutable, so cross-call reuse is safe.
- `formatter.of(code)` returns `undefined` when the input is not a
recognised code AND `fallback: "none"` was specified. The `?? code`
fallback returns the input unchanged in that case.
- OTel histogram `i18n.format.display_name.duration_ms` records the
end-to-end format time per call; counter
`i18n.format.cache_hit{formatter="display_name"}` increments on
cache reuse only (not on first miss).
- Construction-time validation ensures the class never wraps invalid
options. The standalone re-validates on every call (cheap — a Zod
`safeParse` over a 3-field object).

View File

@@ -0,0 +1,175 @@
import { I18nError } from "@suaveplan/i18n-shared";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { resetDisplayNameMetricsState } from "../observability/observability.js";
import {
clearDisplayNameCache,
DisplayNameFormatter,
formatDisplayName,
} from "./formatter.js";
describe("formatDisplayName", () => {
it("returns localised region name", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatDisplayName("GB", "en-US", { type: "region" })).toBe(
"United Kingdom"
);
});
it("returns localised language name", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatDisplayName("fr", "en-US", { type: "language" })).toBe(
"French"
);
});
it("returns localised currency name", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatDisplayName("USD", "en-US", { type: "currency" })).toBe(
"US Dollar"
);
});
it("returns localised script name", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatDisplayName("Latn", "en-US", { type: "script" })).toBe(
"Latin"
);
});
it("returns localised calendar name", { timeout: 5000 }, () => {
clearDisplayNameCache();
const result = formatDisplayName("gregory", "en-US", {
type: "calendar",
});
expect(result.toLowerCase()).toContain("gregorian");
});
it("falls back to code by default for unknown values", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
expect(formatDisplayName("XX", "en-US", { type: "region" })).toBe("XX");
});
it("short style produces abbreviated name", { timeout: 5000 }, () => {
clearDisplayNameCache();
const long = formatDisplayName("US", "en-US", {
type: "region",
style: "long",
});
const short = formatDisplayName("US", "en-US", {
type: "region",
style: "short",
});
expect(long).toBe("United States");
expect(short.length).toBeLessThanOrEqual(long.length);
});
it("clearDisplayNameCache drops cached entries", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
formatDisplayName("GB", "en-US", { type: "region" });
clearDisplayNameCache();
expect(formatDisplayName("GB", "en-US", { type: "region" })).toBe(
"United Kingdom"
);
});
it("fallback: 'none' returns code when Intl returns undefined", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
const result = formatDisplayName("ZZZ", "en-US", {
type: "currency",
fallback: "none",
});
expect(result).toBe("ZZZ");
});
it("formatDisplayName formats region", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatDisplayName("GB", "en-GB", { type: "region" })).toBe(
"United Kingdom"
);
});
it("formatDisplayName output twins DisplayNameFormatter.format output", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
const opts = { type: "language" as const, style: "long" as const };
const standalone = formatDisplayName("fr", "en-GB", opts);
const klass = new DisplayNameFormatter(opts).format("fr", "en-GB");
expect(standalone).toBe(klass);
});
});
describe("DisplayNameFormatter class", () => {
it("DisplayNameFormatter formats language", { timeout: 5000 }, () => {
clearDisplayNameCache();
const f = new DisplayNameFormatter({ type: "language" });
expect(f.format("fr", "en-GB")).toBe("French");
});
it("DisplayNameFormatter rejects missing type with INVALID_FORMATTER_OPTIONS", {
timeout: 5000,
}, () => {
try {
new DisplayNameFormatter({} as never);
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe(
"I18N.INVALID_FORMATTER_OPTIONS"
);
expect((err as I18nError).context).toMatchObject({
field: "type",
});
}
});
});
describe("Intl.DisplayNames RangeError mapping", () => {
it("unsupported locale throws UNSUPPORTED_LOCALE with cause", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
try {
formatDisplayName("fr", "xx-bogus-tag-!!!", { type: "language" });
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(I18nError);
expect((err as I18nError).code).toBe("I18N.UNSUPPORTED_LOCALE");
expect((err as I18nError).cause).toBeInstanceOf(RangeError);
}
});
});
describe("Per-(locale, type, style, fallback) cache", () => {
it("identical key reuses cached Intl.DisplayNames", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
resetDisplayNameMetricsState();
formatDisplayName("fr", "en-GB", { type: "language" });
formatDisplayName("fr", "en-GB", { type: "language" });
// Verified via cache-hit observability counter (asserted in
// observability.test.ts). Smoke check here: second call does
// not throw and returns same value.
const first = formatDisplayName("fr", "en-GB", { type: "language" });
const second = formatDisplayName("fr", "en-GB", { type: "language" });
expect(first).toBe(second);
});
it("distinct type creates distinct cache entries", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
const lang = formatDisplayName("fr", "en-GB", { type: "language" });
// For region, "FR" should map to "France"
const region = formatDisplayName("FR", "en-GB", { type: "region" });
expect(lang).toBe("French");
expect(region).toBe("France");
});
});

View File

@@ -0,0 +1,114 @@
import { I18N_CODES, I18nError } from "@suaveplan/i18n-shared";
import { DisplayNameFormatterOptionsSchema } from "../formatter.schemas.js";
import type {
DisplayNameFormatterCacheKey,
DisplayNameFormatterOptions,
} from "../formatter.types.js";
import {
recordDisplayNameCacheHit,
recordDisplayNameFormat,
} from "../observability/observability.js";
const cache = new Map<DisplayNameFormatterCacheKey, Intl.DisplayNames>();
function buildKey(
locale: string,
options: DisplayNameFormatterOptions
): DisplayNameFormatterCacheKey {
return `${locale}|${options.type}|${options.style ?? "long"}|${
options.fallback ?? "code"
}`;
}
function validateOptions(options: DisplayNameFormatterOptions): void {
const parsed = DisplayNameFormatterOptionsSchema.safeParse(options);
if (parsed.success) return;
const field = String(parsed.error.issues[0]?.path[0] ?? "options");
throw new I18nError(
I18N_CODES.INVALID_FORMATTER_OPTIONS,
`DisplayNameFormatter: invalid options (field: ${field})`,
{ context: { field, issues: parsed.error.issues } }
);
}
function buildIntlFormat(
locale: string,
options: DisplayNameFormatterOptions
): Intl.DisplayNames {
try {
return new Intl.DisplayNames(locale, {
type: options.type,
style: options.style ?? "long",
fallback: options.fallback ?? "code",
});
} catch (cause) {
// Intl.DisplayNames throws RangeError for unsupported locales /
// unsupported option values across every conforming runtime.
throw new I18nError(
I18N_CODES.UNSUPPORTED_LOCALE,
`DisplayNameFormatter: unsupported locale '${locale}'`,
{ cause, context: { locale } }
);
}
}
function getOrCreate(
locale: string,
options: DisplayNameFormatterOptions
): { formatter: Intl.DisplayNames; cached: boolean } {
const key = buildKey(locale, options);
const existing = cache.get(key);
if (existing) return { formatter: existing, cached: true };
const formatter = buildIntlFormat(locale, options);
cache.set(key, formatter);
return { formatter, cached: false };
}
/**
* Format a code (`'GB'`, `'USD'`, `'fr'`, `'gregory'`) as its
* locale-aware display name via `Intl.DisplayNames`.
*
* `options.type` is required and selects the code domain
* (`'language' | 'region' | 'script' | 'currency' | 'calendar' |
* 'dateTimeField'`). Defaults: `style: "long"`, `fallback: "code"`.
*/
export function formatDisplayName(
code: string,
locale: string,
options: DisplayNameFormatterOptions
): string {
validateOptions(options);
const t0 = performance.now();
const { formatter, cached: hit } = getOrCreate(locale, options);
const out = formatter.of(code) ?? code;
if (hit) recordDisplayNameCacheHit();
recordDisplayNameFormat(
locale,
options.type,
options.style ?? "long",
performance.now() - t0
);
return out;
}
/**
* Class wrapper for `formatDisplayName` with construction-time option
* validation and stored defaults.
*/
export class DisplayNameFormatter {
private readonly options: DisplayNameFormatterOptions;
public constructor(options: DisplayNameFormatterOptions) {
validateOptions(options);
this.options = options;
}
public format(code: string, locale: string): string {
return formatDisplayName(code, locale, this.options);
}
}
/** Test-only: clear the per-(locale, type, style, fallback) cache. */
export function clearDisplayNameCache(): void {
cache.clear();
}

View File

@@ -0,0 +1,5 @@
export {
clearDisplayNameCache,
DisplayNameFormatter,
formatDisplayName,
} from "./formatter.js";

View File

@@ -0,0 +1,98 @@
# Display-name ergonomic helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The five ergonomic helpers (`formatLanguageName`, `formatRegionName`,
`formatCurrencyName`, `formatScriptName`, `formatCalendarName`)
pre-bind `formatDisplayName` with the required `type` discriminant so
consumers don't have to remember the discriminant string. Each helper
forwards an optional `style` argument to the underlying call.
The helpers exist because `Intl.DisplayNames` requires the caller to
pass `{ type: "<x>" }` on every call — across a real-world UI codebase
that adds noise and creates a chance to typo a discriminant. The
helpers eliminate both.
## Features
- `formatLanguageName(code, locale, style?)` — pre-binds `type: "language"`
- `formatRegionName(code, locale, style?)` — pre-binds `type: "region"`
- `formatCurrencyName(code, locale, style?)` — pre-binds `type: "currency"`
- `formatScriptName(code, locale, style?)` — pre-binds `type: "script"`
- `formatCalendarName(code, locale, style?)` — pre-binds `type: "calendar"`
- All accept the optional `style` parameter (`"long" | "short" | "narrow"`); when omitted, the underlying `formatDisplayName` defaults to `"long"`.
## Usage: Basic Example
```ts
import {
formatLanguageName,
formatRegionName,
formatCurrencyName,
} from "@suaveplan/i18n-formatter-display-name";
formatLanguageName("fr", "en-GB"); // "French"
formatRegionName("GB", "en-GB"); // "United Kingdom"
formatCurrencyName("USD", "en-GB"); // "US Dollar"
```
## Usage: Advanced Example
```ts
import {
formatLanguageName,
formatRegionName,
formatScriptName,
} from "@suaveplan/i18n-formatter-display-name";
// Build a locale picker — language + region in the user's locale.
const locales = ["fr-FR", "en-GB", "ja-JP", "zh-Hans-CN"];
const userLocale = "en-GB";
const labels = locales.map((tag) => {
const [lang, ...rest] = tag.split("-");
const region = rest[rest.length - 1];
const lname = formatLanguageName(lang, userLocale);
const rname = region ? formatRegionName(region, userLocale) : "";
return rname ? `${lname} (${rname})` : lname;
});
// ["French (France)", "English (United Kingdom)", "Japanese (Japan)", "Chinese (China)"]
// Render script name in narrow style for compact UIs.
const narrow = formatScriptName("Latn", "en-GB", "narrow");
```
## API Reference
Every helper has the signature
`(code: string, locale: string, style?: DisplayNameStyle) => string`.
- **`formatLanguageName(code, locale, style?)`** — Returns the language
display name. `code` is a BCP-47 language subtag (`"fr"`, `"zh"`,
`"en-GB"` is also accepted by the underlying `Intl.DisplayNames`).
- **`formatRegionName(code, locale, style?)`** — Returns the region
display name. `code` is an ISO 3166-1 alpha-2 region code (`"GB"`,
`"US"`).
- **`formatCurrencyName(code, locale, style?)`** — Returns the currency
display name. `code` is an ISO 4217 currency code (`"USD"`,
`"EUR"`).
- **`formatScriptName(code, locale, style?)`** — Returns the script
display name. `code` is an ISO 15924 script code (`"Latn"`,
`"Cyrl"`).
- **`formatCalendarName(code, locale, style?)`** — Returns the calendar
display name. `code` is a Unicode calendar identifier (`"gregory"`,
`"buddhist"`, `"islamic"`).
Every helper throws `I18nError(UNSUPPORTED_LOCALE)` on a malformed
locale tag (matching the standalone semantics).
## Implementation Notes
- Each helper is a one-line wrapper around `formatDisplayName`. There
is no separate cache layer — the underlying `formatDisplayName`
cache by `(locale, type, style, fallback)` covers all five.
- The helpers are pure functions with no shared state.

View File

@@ -0,0 +1,71 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { clearDisplayNameCache } from "../formatter/formatter.js";
import {
formatCalendarName,
formatCurrencyName,
formatLanguageName,
formatRegionName,
formatScriptName,
} from "./helpers.js";
describe("ergonomic helpers", () => {
it("formatLanguageName renders French", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatLanguageName("fr", "en-GB")).toBe("French");
});
it("formatRegionName renders United Kingdom", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatRegionName("GB", "en-GB")).toBe("United Kingdom");
});
it("formatCurrencyName renders US Dollar", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatCurrencyName("USD", "en-GB")).toBe("US Dollar");
});
it("formatScriptName renders Latin", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatScriptName("Latn", "en-GB")).toBe("Latin");
});
it("formatCalendarName renders gregorian display name", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
const result = formatCalendarName("gregory", "en-GB");
expect(result.length).toBeGreaterThan(0);
expect(result.toLowerCase()).toContain("gregorian");
});
it("formatLanguageName forwards style", { timeout: 5000 }, () => {
clearDisplayNameCache();
const long = formatLanguageName("fr", "en-GB", "long");
const short = formatLanguageName("fr", "en-GB", "short");
expect(long).toBe("French");
expect(short.length).toBeGreaterThan(0);
});
it("formatRegionName forwards style", { timeout: 5000 }, () => {
clearDisplayNameCache();
expect(formatRegionName("US", "en-GB", "long")).toBe("United States");
});
it("formatCurrencyName forwards style", { timeout: 5000 }, () => {
clearDisplayNameCache();
const out = formatCurrencyName("USD", "en-GB", "long");
expect(out).toBe("US Dollar");
});
it("formatScriptName forwards style", { timeout: 5000 }, () => {
clearDisplayNameCache();
const out = formatScriptName("Latn", "en-GB", "long");
expect(out).toBe("Latin");
});
it("formatCalendarName forwards style", { timeout: 5000 }, () => {
clearDisplayNameCache();
const out = formatCalendarName("gregory", "en-GB", "long");
expect(out.toLowerCase()).toContain("gregorian");
});
});

View File

@@ -0,0 +1,78 @@
import { formatDisplayName } from "../formatter/formatter.js";
import type { DisplayNameStyle } from "../formatter.types.js";
/**
* Format an ISO 639 language code as a localised language name.
* Pre-binds `formatDisplayName` with `type: "language"`.
*/
export function formatLanguageName(
code: string,
locale: string,
style?: DisplayNameStyle
): string {
return formatDisplayName(code, locale, {
type: "language",
...(style !== undefined ? { style } : {}),
});
}
/**
* Format an ISO 3166 region code as a localised region/country name.
* Pre-binds `formatDisplayName` with `type: "region"`.
*/
export function formatRegionName(
code: string,
locale: string,
style?: DisplayNameStyle
): string {
return formatDisplayName(code, locale, {
type: "region",
...(style !== undefined ? { style } : {}),
});
}
/**
* Format an ISO 4217 currency code as a localised currency name.
* Pre-binds `formatDisplayName` with `type: "currency"`.
*/
export function formatCurrencyName(
code: string,
locale: string,
style?: DisplayNameStyle
): string {
return formatDisplayName(code, locale, {
type: "currency",
...(style !== undefined ? { style } : {}),
});
}
/**
* Format an ISO 15924 script code as a localised script name.
* Pre-binds `formatDisplayName` with `type: "script"`.
*/
export function formatScriptName(
code: string,
locale: string,
style?: DisplayNameStyle
): string {
return formatDisplayName(code, locale, {
type: "script",
...(style !== undefined ? { style } : {}),
});
}
/**
* Format a calendar identifier (`"gregory"`, `"buddhist"`, `"islamic"`,
* etc.) as a localised calendar name. Pre-binds `formatDisplayName`
* with `type: "calendar"`.
*/
export function formatCalendarName(
code: string,
locale: string,
style?: DisplayNameStyle
): string {
return formatDisplayName(code, locale, {
type: "calendar",
...(style !== undefined ? { style } : {}),
});
}

View File

@@ -0,0 +1,7 @@
export {
formatCalendarName,
formatCurrencyName,
formatLanguageName,
formatRegionName,
formatScriptName,
} from "./helpers.js";

View File

@@ -0,0 +1,97 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "@suaveplan/testing/runner";
import * as root from "./index.js";
describe("@suaveplan/i18n-formatter-display-name root barrel", () => {
it("re-exports DisplayNameFormatter", { timeout: 5000 }, () => {
expect(root.DisplayNameFormatter).toBeDefined();
});
it("re-exports formatDisplayName", { timeout: 5000 }, () => {
expect(typeof root.formatDisplayName).toBe("function");
});
it("re-exports clearDisplayNameCache", { timeout: 5000 }, () => {
expect(typeof root.clearDisplayNameCache).toBe("function");
});
it("re-exports DisplayNameFormatterOptionsSchema", {
timeout: 5000,
}, () => {
expect(root.DisplayNameFormatterOptionsSchema).toBeDefined();
});
it("re-exports five ergonomic helpers", { timeout: 5000 }, () => {
expect(typeof root.formatLanguageName).toBe("function");
expect(typeof root.formatRegionName).toBe("function");
expect(typeof root.formatCurrencyName).toBe("function");
expect(typeof root.formatScriptName).toBe("function");
expect(typeof root.formatCalendarName).toBe("function");
});
it("re-exports observability surface", { timeout: 5000 }, () => {
expect(typeof root.recordDisplayNameFormat).toBe("function");
expect(typeof root.recordDisplayNameCacheHit).toBe("function");
expect(typeof root.getDisplayNameMetricsState).toBe("function");
expect(typeof root.resetDisplayNameMetricsState).toBe("function");
});
it("every documented public export resolves", { timeout: 5000 }, () => {
const expectedSurface = [
"DisplayNameFormatter",
"formatDisplayName",
"clearDisplayNameCache",
"DisplayNameFormatterOptionsSchema",
"formatLanguageName",
"formatRegionName",
"formatCurrencyName",
"formatScriptName",
"formatCalendarName",
"recordDisplayNameFormat",
"recordDisplayNameCacheHit",
"getDisplayNameMetricsState",
"resetDisplayNameMetricsState",
];
const exported = Object.keys(root);
for (const name of expectedSurface) {
expect(exported).toContain(name);
}
});
});
describe("@suaveplan/i18n-formatter-display-name subpath imports", () => {
it("every documented subpath export resolves", {
timeout: 5000,
}, async () => {
const formatter = await import("./formatter/formatter.js");
expect(formatter.DisplayNameFormatter).toBeDefined();
expect(typeof formatter.formatDisplayName).toBe("function");
const helpers = await import("./helpers/helpers.js");
expect(typeof helpers.formatLanguageName).toBe("function");
});
});
const PACKAGE_JSON_PATH = fileURLToPath(
new URL("../package.json", import.meta.url)
);
describe("package.json invariants", () => {
it("package.json declares sideEffects false", { timeout: 5000 }, () => {
const raw = readFileSync(PACKAGE_JSON_PATH, "utf8");
const pkg = JSON.parse(raw) as { sideEffects?: boolean };
expect(pkg.sideEffects).toBe(false);
});
it("package.json declares the three documented subpath exports", {
timeout: 5000,
}, () => {
const raw = readFileSync(PACKAGE_JSON_PATH, "utf8");
const pkg = JSON.parse(raw) as { exports?: Record<string, unknown> };
const exports = pkg.exports ?? {};
const required = [".", "./formatter", "./helpers"];
for (const subpath of required) {
expect(exports).toHaveProperty(subpath);
}
});
});

View File

@@ -0,0 +1,31 @@
// Root barrel for @suaveplan/i18n-formatter-display-name. Explicit
// named re-exports only — no `export *` per CLAUDE.md §10.
export {
clearDisplayNameCache,
DisplayNameFormatter,
formatDisplayName,
} from "./formatter/index.js";
export { DisplayNameFormatterOptionsSchema } from "./formatter.schemas.js";
export type {
DisplayNameFallback,
DisplayNameFormatterCacheKey,
DisplayNameFormatterOptions,
DisplayNameStyle,
DisplayNameType,
} from "./formatter.types.js";
export {
formatCalendarName,
formatCurrencyName,
formatLanguageName,
formatRegionName,
formatScriptName,
} from "./helpers/index.js";
export {
type DisplayNameMetricSample,
type DisplayNameMetricsState,
getDisplayNameMetricsState,
recordDisplayNameCacheHit,
recordDisplayNameFormat,
resetDisplayNameMetricsState,
} from "./observability/index.js";

View File

@@ -0,0 +1,10 @@
export type {
DisplayNameMetricSample,
DisplayNameMetricsState,
} from "./observability.js";
export {
getDisplayNameMetricsState,
recordDisplayNameCacheHit,
recordDisplayNameFormat,
resetDisplayNameMetricsState,
} from "./observability.js";

View File

@@ -0,0 +1,98 @@
# DisplayName observability
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The DisplayName formatter exports two OpenTelemetry instruments and a
small in-memory state ring so spec-conformance tests can assert exact
attribute payloads without standing up a full SDK exporter. The
instruments match the canonical Wave-3 formatter contract — every
sibling formatter (number, relative-time, list, date, currency,
collator, segmenter) emits the same two metric names so dashboards
and alerts compose cleanly across the family.
## Features
- Histogram `i18n.format.display_name.duration_ms` (unit `ms`) — per-format wall-clock time
- Counter `i18n.format.cache_hit{formatter="display_name"}` — increments on cache reuse only
- In-memory state ring (`getDisplayNameMetricsState()`) for spec-conformance assertions
- `resetDisplayNameMetricsState()` test-only helper to clear the ring between cases
## Usage: Basic Example
```ts
import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
// import your MetricReader / OTLP exporter ...
const provider = new MeterProvider({ readers: [/* reader */] });
metrics.setGlobalMeterProvider(provider);
// Subsequent formatDisplayName calls now emit through your reader.
import { formatDisplayName } from "@suaveplan/i18n-formatter-display-name";
formatDisplayName("fr", "en-GB", { type: "language" });
```
## Usage: Advanced Example
```ts
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
clearDisplayNameCache,
formatDisplayName,
} from "@suaveplan/i18n-formatter-display-name";
import {
getDisplayNameMetricsState,
resetDisplayNameMetricsState,
} from "@suaveplan/i18n-formatter-display-name";
it("emits histogram + cache-hit counter", () => {
clearDisplayNameCache();
resetDisplayNameMetricsState();
formatDisplayName("fr", "en-GB", { type: "language" });
formatDisplayName("fr", "en-GB", { type: "language" });
const state = getDisplayNameMetricsState();
expect(state.durationSamples).toHaveLength(2);
expect(state.cacheHitSamples).toHaveLength(1);
});
```
## API Reference
### `recordDisplayNameFormat(locale, type, style, durationMs): void`
Records the duration histogram sample. Tags: `locale`, `type`,
`style`. Called internally by `formatDisplayName`; consumers do not
need to invoke it directly.
### `recordDisplayNameCacheHit(): void`
Increments the cache-hit counter with the constant tag
`{ formatter: "display_name" }`. Called internally on a cache hit.
### `getDisplayNameMetricsState(): DisplayNameMetricsState`
Returns the in-memory ring of `durationSamples` and `cacheHitSamples`.
Used by spec-conformance tests to assert exact attribute payloads.
### `resetDisplayNameMetricsState(): void`
Clears both ring buffers. Test-only helper; call before the
under-test code to assert exactly the samples your scenario
generates.
## Implementation Notes
- The OTel meter is named `suaveplan.i18n.formatter.display_name`.
Every formatter in the family has its own meter so OTel resource
attributes can be tied per-package if a consumer chooses.
- Sample dashboard query (PromQL/Grafana over OTLP):
`histogram_quantile(0.99, sum(rate(i18n_format_display_name_duration_ms_bucket[5m])) by (le, locale))`
- The in-memory ring is intentionally unbounded — tests are short and
call `resetDisplayNameMetricsState()` between cases. A long-running
test loop should call reset to avoid memory growth.
- `style` defaults to `"long"` in the histogram tag when the caller
omits it; `type` is always required so it is never defaulted.

View File

@@ -0,0 +1,53 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
clearDisplayNameCache,
formatDisplayName,
} from "../formatter/formatter.js";
import {
getDisplayNameMetricsState,
resetDisplayNameMetricsState,
} from "./observability.js";
describe("DisplayName observability", () => {
it("emits i18n.format.display_name.duration_ms histogram on every call", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
resetDisplayNameMetricsState();
formatDisplayName("fr", "en-GB", { type: "language", style: "long" });
const state = getDisplayNameMetricsState();
expect(state.durationSamples).toHaveLength(1);
expect(state.durationSamples[0]?.attributes).toMatchObject({
locale: "en-GB",
type: "language",
style: "long",
});
});
it("cache-hit counter increments only on second call with same options", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
resetDisplayNameMetricsState();
formatDisplayName("fr", "en-GB", { type: "language" });
// First call should NOT emit a cache-hit.
expect(getDisplayNameMetricsState().cacheHitSamples).toHaveLength(0);
formatDisplayName("fr", "en-GB", { type: "language" });
// Second call SHOULD emit exactly one cache-hit.
const state = getDisplayNameMetricsState();
expect(state.cacheHitSamples).toHaveLength(1);
expect(state.cacheHitSamples[0]?.attributes.formatter).toBe(
"display_name"
);
});
it("histogram defaults style label to 'long' when style omitted", {
timeout: 5000,
}, () => {
clearDisplayNameCache();
resetDisplayNameMetricsState();
formatDisplayName("fr", "en-GB", { type: "language" });
const state = getDisplayNameMetricsState();
expect(state.durationSamples[0]?.attributes.style).toBe("long");
});
});

View File

@@ -0,0 +1,82 @@
import { type Counter, type Histogram, metrics } from "@opentelemetry/api";
/**
* OTel instruments for the DisplayName formatter.
*
* Histogram records per-format wall-clock duration; the counter
* increments on every cache reuse so consumers can observe cache
* effectiveness.
*
* The module retains a small in-memory state ring so spec-conformance
* tests can assert exact attribute payloads without standing up a
* full SDK exporter.
*/
export interface DisplayNameMetricSample {
readonly attributes: Record<string, string | number>;
}
export interface DisplayNameMetricsState {
readonly durationSamples: DisplayNameMetricSample[];
readonly cacheHitSamples: DisplayNameMetricSample[];
}
let cached:
| {
readonly durationMs: Histogram;
readonly cacheHit: Counter;
}
| undefined;
const state: DisplayNameMetricsState = {
durationSamples: [],
cacheHitSamples: [],
};
function ensureInstruments() {
if (cached) return cached;
const meter = metrics.getMeter("suaveplan.i18n.formatter.display_name");
cached = {
durationMs: meter.createHistogram(
"i18n.format.display_name.duration_ms",
{
description:
"Duration in ms of DisplayNameFormatter.format; labels: locale, type, style.",
unit: "ms",
}
),
cacheHit: meter.createCounter("i18n.format.cache_hit", {
description:
"Format-time cache reuse counter; labels: formatter (display_name).",
}),
};
return cached;
}
export function recordDisplayNameFormat(
locale: string,
type: string,
style: string,
durationMs: number
): void {
const ins = ensureInstruments();
const attributes = { locale, type, style };
ins.durationMs.record(durationMs, attributes);
state.durationSamples.push({ attributes });
}
export function recordDisplayNameCacheHit(): void {
const ins = ensureInstruments();
const attributes = { formatter: "display_name" };
ins.cacheHit.add(1, attributes);
state.cacheHitSamples.push({ attributes });
}
export function getDisplayNameMetricsState(): DisplayNameMetricsState {
return state;
}
export function resetDisplayNameMetricsState(): void {
state.durationSamples.length = 0;
state.cacheHitSamples.length = 0;
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,38 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/i18n-formatter-display-name",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@suaveplan/error",
"@suaveplan/i18n-shared",
"@suaveplan/types",
"@opentelemetry/api",
"tslib",
"zod",
/^node:/,
],
},
},
});

View File

@@ -0,0 +1,29 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
include: ["src/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.types.ts",
"src/**/*.schemas.ts",
"src/**/index.ts",
],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
testTimeout: 5000,
},
});