Files
suaveplan/openspec/archive/2026-05-09-i18n-formatter-list/proposal.md
2026-05-27 23:29:53 +01:00

97 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `@suaveplan/i18n-formatter-list` — `Intl.ListFormat` wrapper
**Package**: `@suaveplan/i18n-formatter-list` (NEW)
**Status**: PROPOSED
**Type**: feature (new universal-tier package; verbatim port of `formatting/list/` 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-list/`
**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-list` ports the existing `formatting/list/formatList` standalone helper, adds the `ListFormatter` class wrapper that the rest of the formatter family ships, and brings the package up to the canonical Wave-3 shape (Zod schema, OTel histogram + cache-hit counter, sibling test files).
Today `formatting/list/` ships a thin slice:
- `formatList(values, locale, options?)` standalone helper (`packages/i18n/i18n/src/formatting/list/list.ts` L41–L49)
- `clearListFormatterCache` test-only helper (`list.ts` L52–L54)
- Per-(locale, style, type) cache (`list.ts` L12, L14–L33)
- `UNSUPPORTED_LOCALE` mapping (`list.ts` L26–L31)
- `ListFormatStyle` and `ListFormatType` type aliases (`list.ts` L4–L5)
The split pulls these into `i18n-formatter-list`, adds the class wrapper for symmetry with `NumberFormatter` / `RelativeTimeFormatter` / `DateFormatter`, adds `INVALID_FORMATTER_OPTIONS` Zod-validation on options, and wires the OTel histogram + cache-hit counter so the family is consistent.
## What changes
### New package: `@suaveplan/i18n-formatter-list`
- ADDED: `packages/i18n/i18n-formatter-list/` package scaffold (`package.json`, `tsconfig.json`, `vite.config.ts`, `README.md`).
- ADDED: `src/formatter.ts` — `ListFormatter` class (NEW, symmetric with sibling formatters) wrapping `Intl.ListFormat` with construction-time Zod option validation; `formatList` standalone (port of monolith `list.ts` L41–L49); `clearListFormatterCache` test-only helper (port of L52–L54). Per-(locale, style, type) cache key `${locale}|${style}|${type}` (port of L19).
- ADDED: `src/formatter.types.ts` — `ListFormatStyle` (`"long" | "short" | "narrow"`), `ListFormatType` (`"conjunction" | "disjunction" | "unit"`), `ListFormatterOptions`, `ListFormatterCacheKey`. Sourced from monolith `list.ts` L4–L10 plus new types for the class surface.
- ADDED: `src/formatter.schemas.ts` — `ListFormatterOptionsSchema` Zod schema. NEW (the monolith does not currently Zod-validate list options).
- ADDED: `src/helpers.ts` — `formatConjunction(values, locale, style?)` and `formatDisjunction(values, locale, style?)` ergonomic shortcuts that pre-bind `formatList` with `type: "conjunction"` / `type: "disjunction"`.
- ADDED: `src/observability.ts` — local OTel histogram `i18n.format.list.duration_ms` + counter `i18n.format.cache_hit{formatter="list"}`.
- ADDED: `src/formatter.test.ts` — port of monolith `list.test.ts` (56 lines) plus new tests covering `ListFormatter` class wrapper, Zod-validation, and cache-hit behaviour.
- ADDED: `src/formatter.spec-conformance.test.ts` — NEW spec-conformance suite asserting every requirement in this delta.
- ADDED: `src/helpers.test.ts` — covers `formatConjunction` and `formatDisjunction`.
- 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` — `ListFormatter` class + `formatList` + types + schema only
- `./helpers` — `formatConjunction`, `formatDisjunction` 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/list/` from `packages/i18n/i18n/src/`** — happens in Wave 6.
- **`formatToParts` for list output** — follow-up; current monolith does not expose it.
- **Custom list separators** — out of scope; `Intl.ListFormat` already handles locale-correct separators.
## Source mapping
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `formatList` standalone | `packages/i18n/i18n/src/formatting/list/list.ts` | L41–L49 | Verbatim port |
| `clearListFormatterCache` | `packages/i18n/i18n/src/formatting/list/list.ts` | L52–L54 | Verbatim port |
| Per-(locale, style, type) cache | `packages/i18n/i18n/src/formatting/list/list.ts` | L12, L14–L33 | Verbatim port |
| `UNSUPPORTED_LOCALE` mapping | `packages/i18n/i18n/src/formatting/list/list.ts` | L22–L31 | Verbatim port; throws `I18nError` from `@suaveplan/i18n-shared` |
| `ListFormatStyle`, `ListFormatType`, `ListFormatOptions` types | `packages/i18n/i18n/src/formatting/list/list.ts` | L4–L10 | Verbatim port; rename `ListFormatOptions` → `ListFormatterOptions` for symmetry |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `ListFormatter` class wrapper | New — symmetric with `NumberFormatter`, `RelativeTimeFormatter`, `DateFormatter` so consumers can construct once with stable options and reuse |
| `ListFormatterOptionsSchema` Zod schema | New — current monolith does not validate list options; the canonical Wave-3 formatter shape mandates Zod-validated boundaries |
| `INVALID_FORMATTER_OPTIONS` mapping | Bound to the new Zod schema |
| OTel histogram `i18n.format.list.duration_ms` + cache-hit counter | New — the canonical Wave-3 formatter shape mandates these (the monolith currently does not emit them for the list formatter) |
| `formatConjunction`, `formatDisjunction` helpers | New ergonomic surface symmetric with `formatPercent` / `formatCompact` for the number formatter |
## Verification approach
- `packages/i18n/i18n-formatter-list/src/formatter.test.ts` — port of monolith `list.test.ts`; covers conjunction / disjunction / unit types; long / short / narrow styles; default `(long, conjunction)`; class wrapper twins helper output; Zod failure → `INVALID_FORMATTER_OPTIONS`; bogus locale → `UNSUPPORTED_LOCALE`; cache reuse on identical key
- `packages/i18n/i18n-formatter-list/src/formatter.spec-conformance.test.ts` — every requirement scenario in this delta runs as an executable test
- `packages/i18n/i18n-formatter-list/src/helpers.test.ts` — `formatConjunction(["a", "b", "c"], "en-GB")` returns `"a, b, and c"`; `formatDisjunction(["x", "y"], "en-GB")` returns `"x or y"`
- `packages/i18n/i18n-formatter-list/src/observability.test.ts` — histogram named `i18n.format.list.duration_ms`; counter `i18n.format.cache_hit{formatter="list"}` increments only on cache hit
- `packages/i18n/i18n-formatter-list/src/index.test.ts` — every documented public export resolves; subpaths resolve
## Impact
- New universal-tier package — joins the rest of the i18n family under `packages/i18n/`.
- Indirect consumer in Wave 6: `@suaveplan/i18n` slim core re-exports `ListFormatter` and `formatList` under the back-compat root barrel.
- `MANIFEST.md` and `CLAUDE.md` updated post-merge.