first commit
This commit is contained in:
107
docs/localization/i18n/i18n-formatter-list/README.md
Normal file
107
docs/localization/i18n/i18n-formatter-list/README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# @suaveplan/i18n-formatter-list - Locale-aware list joining backed by Intl.ListFormat. Class wrapper + standalone helper + ergonomic helpers (formatConjunction, formatDisjunction), Zod-validated options, per-(locale, style, type) cache, OTel histogram + cache-hit counter. Wave 3 of the @suaveplan/i18n modular split.
|
||||
|
||||
## Summary
|
||||
|
||||
Locale-aware list joining backed by `Intl.ListFormat`. Wave 3 of the
|
||||
14-package i18n modular split. Provides both a class
|
||||
(`ListFormatter`) and a standalone helper (`formatList`) plus
|
||||
ergonomic shortcuts (`formatConjunction`, `formatDisjunction`) on
|
||||
top of a per-`(locale, style, type)` cache, Zod-validated options,
|
||||
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 list joining backed by `Intl.ListFormat`. Joins an array of strings with the locale-correct connector (`"a…
|
||||
- [formatter](./src/formatter/formatter.md) — Locale-aware list joining backed by `Intl.ListFormat`. Joins an array of strings with the locale-correct connector (`"a…
|
||||
- [helpers](./src/helpers/helpers.md) — Pre-bound shortcuts over `formatList` for the two most common list-formatting shapes: conjunction joining ("a, b, and c…
|
||||
- [helpers](./src/helpers/helpers.md) — Pre-bound shortcuts over `formatList` for the two most common list-formatting shapes: conjunction joining ("a, b, and c…
|
||||
- [observability](./src/observability/observability.md) — Per-package OpenTelemetry instruments for `@suaveplan/i18n-formatter-list`. Exposes a histogram (`i18n.format.list.dura…
|
||||
- [observability](./src/observability/observability.md) — Per-package OpenTelemetry instruments for `@suaveplan/i18n-formatter-list`. Exposes a histogram (`i18n.format.list.dura…
|
||||
<!-- 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
|
||||
list-formatter slice of Wave 3. The monolith ships only a thin
|
||||
standalone helper and per-cache module — this package brings the
|
||||
canonical Wave 3 shape on top: class wrapper for symmetry with
|
||||
`NumberFormatter`, Zod schema validating options at the boundary,
|
||||
mapping of Zod failure to `INVALID_FORMATTER_OPTIONS`, and OTel
|
||||
emission for parity with the rest of the family.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/i18n-formatter-list
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import {
|
||||
formatConjunction,
|
||||
formatDisjunction,
|
||||
formatList,
|
||||
ListFormatter,
|
||||
} from "@suaveplan/i18n-formatter-list";
|
||||
|
||||
formatList(["apples", "oranges", "bananas"], "en-US");
|
||||
// → "apples, oranges, and bananas"
|
||||
|
||||
formatList(["apples", "oranges"], "en-US", { type: "disjunction" });
|
||||
// → "apples or oranges"
|
||||
|
||||
formatConjunction(["a", "b", "c"], "en-GB"); // → "a, b, and c"
|
||||
formatDisjunction(["x", "y"], "en-GB"); // → "x or y"
|
||||
|
||||
const compact = new ListFormatter({ style: "short", type: "unit" });
|
||||
compact.format(["mon", "tue", "wed"], "en-US"); // → "mon, tue, wed"
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
| Subpath | Description |
|
||||
|---|---|
|
||||
| `@suaveplan/i18n-formatter-list` | Root barrel — class + standalone + helpers + observability |
|
||||
| `@suaveplan/i18n-formatter-list/formatter` | `ListFormatter`, `formatList`, `clearListFormatterCache`, types, schema |
|
||||
| `@suaveplan/i18n-formatter-list/helpers` | `formatConjunction`, `formatDisjunction` only |
|
||||
|
||||
## Modules Documentation
|
||||
|
||||
- [formatter.md](src/formatter.md) — `ListFormatter` class + `formatList`
|
||||
- [helpers.md](src/helpers.md) — `formatConjunction`, `formatDisjunction`
|
||||
- [observability.md](src/observability.md) — OTel histogram + cache-hit counter
|
||||
|
||||
## Public Surface (snapshot)
|
||||
|
||||
| Export | Kind | Origin |
|
||||
|---|---|---|
|
||||
| `ListFormatter` | class | `formatter.ts` |
|
||||
| `formatList` | function | `formatter.ts` |
|
||||
| `clearListFormatterCache` | function | `formatter.ts` |
|
||||
| `ListFormatterOptionsSchema` | const | `formatter.ts` |
|
||||
| `ListFormatStyle` | type | `formatter.types.ts` |
|
||||
| `ListFormatType` | type | `formatter.types.ts` |
|
||||
| `ListFormatterOptions` | type | `formatter.types.ts` |
|
||||
| `ListFormatterCacheKey` | type | `formatter.types.ts` |
|
||||
| `formatConjunction` | function | `helpers.ts` |
|
||||
| `formatDisjunction` | function | `helpers.ts` |
|
||||
| `recordListFormat` | function | `observability.ts` |
|
||||
| `recordListCacheHit` | function | `observability.ts` |
|
||||
| `getListFormatMetricsState` | function | `observability.ts` |
|
||||
| `resetListFormatMetricsState` | 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. Pure
|
||||
addition — `packages/i18n/i18n/src/formatting/list/` is preserved
|
||||
in the monolith until Wave 6 deletes it.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# ListFormatter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Locale-aware list joining backed by `Intl.ListFormat`. Joins an array of strings with the locale-correct connector (`"apples, oranges, and bananas"` in `en-US`). Supports conjunction (`and`), disjunction (`or`), and unit list types, plus three styles (`long`, `short`, `narrow`).
|
||||
|
||||
This package is the Wave 3 list-formatter slice — the monolith ships only the standalone helper and per-cache module; this package adds the canonical Wave 3 shape on top: a `ListFormatter` class wrapper for symmetry with `NumberFormatter` / `RelativeTimeFormatter` / `DateFormatter`, a Zod schema validating options at the boundary, mapping of `Intl` `RangeError` to `I18N.UNSUPPORTED_LOCALE`, mapping of Zod failure to `I18N.INVALID_FORMATTER_OPTIONS`, and an OTel histogram + cache-hit counter for per-formatter parity.
|
||||
|
||||
The package caches `Intl.ListFormat` per `(locale, style, type)` so dashboards rendering many comma-separated lists do not pay the construction cost on every call. Cache reuse is opaque to callers and increments `i18n.format.cache_hit{formatter="list"}`.
|
||||
|
||||
## Features
|
||||
|
||||
- Class form (`ListFormatter`) for fixed-options reuse + standalone helper (`formatList`) for one-off calls
|
||||
- `style: 'long' | 'short' | 'narrow'` (default `'long'`)
|
||||
- `type: 'conjunction' | 'disjunction' | 'unit'` (default `'conjunction'`)
|
||||
- Zod-validated options at construction; unknown style/type rejected with structured `INVALID_FORMATTER_OPTIONS`
|
||||
- Stable error codes: `I18N.INVALID_FORMATTER_OPTIONS`, `I18N.UNSUPPORTED_LOCALE`
|
||||
- Process-wide `Intl.ListFormat` cache; `clearListFormatterCache()` for tests
|
||||
- OTel instrumentation: `i18n.format.list.duration_ms` histogram, `i18n.format.cache_hit{formatter="list"}` counter
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { formatList, ListFormatter } from "@suaveplan/i18n-formatter-list";
|
||||
|
||||
formatList(["apples", "oranges", "bananas"], "en-US");
|
||||
// → "apples, oranges, and bananas"
|
||||
|
||||
formatList(["apples", "oranges"], "en-US", { type: "disjunction" });
|
||||
// → "apples or oranges"
|
||||
|
||||
const compact = new ListFormatter({ style: "short", type: "unit" });
|
||||
compact.format(["mon", "tue", "wed"], "en-US");
|
||||
// → "mon, tue, wed"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example — Building Tag Lists
|
||||
|
||||
```typescript
|
||||
import { ListFormatter } from "@suaveplan/i18n-formatter-list";
|
||||
|
||||
const tagList = new ListFormatter({ type: "unit", style: "short" });
|
||||
function summariseTags(items, locale) {
|
||||
const tags = items.map((item) => item.label);
|
||||
return `Tagged: ${tagList.format(tags, locale)}`;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `formatList(values, locale, options?): string`
|
||||
|
||||
Joins `values` per `locale` and `options`. Caches the underlying `Intl.ListFormat` per `(locale, style, type)`. Validates options through Zod, throws `I18nError(I18N.INVALID_FORMATTER_OPTIONS)` on failure. Maps `Intl.ListFormat`'s `RangeError` for unsupported locales to `I18nError(I18N.UNSUPPORTED_LOCALE)`.
|
||||
|
||||
### `class ListFormatter`
|
||||
|
||||
- `constructor(options?: ListFormatterOptions)` — validates options at construction
|
||||
- `format(values: readonly string[], locale: string): string` — formats with stored options
|
||||
|
||||
### `clearListFormatterCache(): void`
|
||||
|
||||
Test-only.
|
||||
|
||||
### `ListFormatterOptionsSchema`
|
||||
|
||||
Zod schema validating the `ListFormatterOptions` surface.
|
||||
|
||||
### Types
|
||||
|
||||
`ListFormatStyle`, `ListFormatType`, `ListFormatterOptions`, `ListFormatterCacheKey`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Intl.ListFormat` accepts a readonly array but the type signature on Node 22+ requires a mutable array; we cast at the boundary. The cast is safe because `Intl.ListFormat.format` reads but does not mutate the input.
|
||||
|
||||
`type: 'unit'` is intended for units of measurement (e.g. measurement parts in `formatNumber({ style: 'unit' })`). For free-form lists prefer `'conjunction'` or `'disjunction'`.
|
||||
|
||||
The cache key is `${locale}|${style}|${type}` rather than a JSON-stringified options bag because `Intl.ListFormat` has only three knobs. This is symmetric with the `RelativeTimeFormatter` cache strategy (and distinct from the `NumberFormatter` cache strategy which uses JSON-stringified options because the option surface is much wider).
|
||||
@@ -0,0 +1,60 @@
|
||||
# List Helper Formatters
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Pre-bound shortcuts over `formatList` for the two most common list-formatting shapes: conjunction joining ("a, b, and c") and disjunction joining ("a, b, or c"). These are net-new ergonomic helpers introduced by the Wave-3 split — symmetric with `formatPercent` / `formatCompact` for the number formatter.
|
||||
|
||||
The helpers route every call through `formatList`, so they inherit Zod option validation, the per-`(locale, style, type)` cache, and the OTel histogram + cache-hit counter from the formatter module.
|
||||
|
||||
## Features
|
||||
|
||||
- `formatConjunction(values, locale, style?)` — pre-bound `formatList({ type: 'conjunction', style })`
|
||||
- `formatDisjunction(values, locale, style?)` — pre-bound `formatList({ type: 'disjunction', style })`
|
||||
- Style argument defaults to `'long'`; pass `'short'` or `'narrow'` for compact rendering
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
formatConjunction,
|
||||
formatDisjunction,
|
||||
} from "@suaveplan/i18n-formatter-list/helpers";
|
||||
|
||||
formatConjunction(["a", "b", "c"], "en-GB"); // → "a, b, and c"
|
||||
formatConjunction(["x", "y"], "en-GB", "short"); // → "x, & y" or "x & y" (locale-dependent)
|
||||
|
||||
formatDisjunction(["red", "green", "blue"], "en-GB"); // → "red, green, or blue"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example — Permission summary
|
||||
|
||||
```typescript
|
||||
import { formatConjunction } from "@suaveplan/i18n-formatter-list/helpers";
|
||||
|
||||
function summarisePermissions(permissions, locale) {
|
||||
const labels = permissions.map((p) => p.label);
|
||||
return labels.length === 0
|
||||
? "No permissions"
|
||||
: `Permissions: ${formatConjunction(labels, locale)}`;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `formatConjunction(values, locale, style?)`
|
||||
|
||||
Pre-bound `formatList({ type: 'conjunction', style })`. Style defaults to `'long'`. Inherits all error mapping (`INVALID_FORMATTER_OPTIONS`, `UNSUPPORTED_LOCALE`) and observability (histogram + cache-hit counter) from `formatList`.
|
||||
|
||||
### `formatDisjunction(values, locale, style?)`
|
||||
|
||||
Pre-bound `formatList({ type: 'disjunction', style })`. Style defaults to `'long'`. Same inheritance as `formatConjunction`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The helpers take `style` as a positional third argument rather than an options bag because `formatList`'s only meaningful per-call option (apart from `type`, which is fixed by the helper) is `style`. Keeping the helper signature lean makes the call site read as "what kind of join, in which language, in which style" — a natural sentence shape.
|
||||
|
||||
The two helpers are intentionally simple wrappers because the slim core (Wave 6) wires `formatList` (not the helpers) into the default `FormatBridge` for ICU `{values, list, type=conjunction}` placeholder rendering. Adding more transformation logic in the helpers would create drift between what the slim core renders and what direct callers see.
|
||||
@@ -0,0 +1,91 @@
|
||||
# ListFormatter Observability
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Per-package OpenTelemetry instruments for `@suaveplan/i18n-formatter-list`. Exposes a histogram (`i18n.format.list.duration_ms`) recording wall-clock duration of every `formatList` call (tagged with `locale`, `style`, and `type`) and a counter (`i18n.format.cache_hit`) tagged with `formatter: "list"` that increments on every cache reuse.
|
||||
|
||||
The list formatter introduces these instruments as part of the Wave 3 uplift — the monolith currently emits no OTel for list 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.list.duration_ms` with attributes `{ locale, style, type }`
|
||||
- Stable counter name `i18n.format.cache_hit` with attributes `{ formatter: "list" }`
|
||||
- In-memory sample ring (`listDurationSamples`, `cacheHitSamples`)
|
||||
- `resetListFormatMetricsState()` clears both arrays so tests stay isolated
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
recordListFormat,
|
||||
recordListCacheHit,
|
||||
getListFormatMetricsState,
|
||||
resetListFormatMetricsState,
|
||||
} from "@suaveplan/i18n-formatter-list/observability";
|
||||
|
||||
resetListFormatMetricsState();
|
||||
recordListFormat("en-GB", "long", "conjunction", 0.4);
|
||||
recordListCacheHit();
|
||||
const state = getListFormatMetricsState();
|
||||
state.listDurationSamples.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 {
|
||||
formatList,
|
||||
clearListFormatterCache,
|
||||
} from "@suaveplan/i18n-formatter-list";
|
||||
import {
|
||||
getListFormatMetricsState,
|
||||
resetListFormatMetricsState,
|
||||
} from "@suaveplan/i18n-formatter-list/observability";
|
||||
|
||||
describe("metrics emission", () => {
|
||||
it("emits duration histogram and cache-hit counter", { timeout: 5000 }, () => {
|
||||
clearListFormatterCache();
|
||||
resetListFormatMetricsState();
|
||||
formatList(["a", "b"], "en-GB");
|
||||
formatList(["a", "b"], "en-GB");
|
||||
const state = getListFormatMetricsState();
|
||||
expect(state.listDurationSamples).toHaveLength(2);
|
||||
expect(state.cacheHitSamples).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `recordListFormat(locale, style, type, durationMs): void`
|
||||
|
||||
Records one observation on the `i18n.format.list.duration_ms` histogram and pushes a `{ attributes: { locale, style, type } }` entry into the in-memory sample ring.
|
||||
|
||||
### `recordListCacheHit(): void`
|
||||
|
||||
Adds 1 to the `i18n.format.cache_hit` counter with `formatter: "list"` and pushes a matching entry into the in-memory sample ring.
|
||||
|
||||
### `getListFormatMetricsState(): ListFormatMetricsState`
|
||||
|
||||
Returns the in-memory sample ring (live reference — do not mutate).
|
||||
|
||||
### `resetListFormatMetricsState(): void`
|
||||
|
||||
Empties both sample arrays in place.
|
||||
|
||||
### `ListFormatMetricsState`
|
||||
|
||||
`{ listDurationSamples: FormatMetricSample[]; cacheHitSamples: FormatMetricSample[] }` where `FormatMetricSample = { attributes: Record<string, string | number> }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The instruments are built lazily so consumers who never call `formatList` 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 `resetListFormatMetricsState` calls.
|
||||
|
||||
This module is one of two Wave 3 changes that introduce a *new* OTel surface — the monolith's `formatting/list/` module emits nothing, while `formatting/number/` and `formatting/relative-time/` already emit through the shared `observability/format-metrics.ts` module. The Wave 3 split brings every formatter package up to the same canonical observability shape so the slim core (Wave 6) can dashboard the entire family with a single regex on `i18n.format.*`.
|
||||
Reference in New Issue
Block a user