# `@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.