7.3 KiB
@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.tsL41–L49)clearListFormatterCachetest-only helper (list.tsL52–L54)- Per-(locale, style, type) cache (
list.tsL12, L14–L33) UNSUPPORTED_LOCALEmapping (list.tsL26–L31)ListFormatStyleandListFormatTypetype aliases (list.tsL4–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—ListFormatterclass (NEW, symmetric with sibling formatters) wrappingIntl.ListFormatwith construction-time Zod option validation;formatListstandalone (port of monolithlist.tsL41–L49);clearListFormatterCachetest-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 monolithlist.tsL4–L10 plus new types for the class surface. - ADDED:
src/formatter.schemas.ts—ListFormatterOptionsSchemaZod schema. NEW (the monolith does not currently Zod-validate list options). - ADDED:
src/helpers.ts—formatConjunction(values, locale, style?)andformatDisjunction(values, locale, style?)ergonomic shortcuts that pre-bindformatListwithtype: "conjunction"/type: "disjunction". - ADDED:
src/observability.ts— local OTel histogrami18n.format.list.duration_ms+ counteri18n.format.cache_hit{formatter="list"}. - ADDED:
src/formatter.test.ts— port of monolithlist.test.ts(56 lines) plus new tests coveringListFormatterclass 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— coversformatConjunctionandformatDisjunction. - 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—ListFormatterclass +formatList+ types + schema only./helpers—formatConjunction,formatDisjunctiononly
Dependencies
@suaveplan/i18n-shared workspace:^—I18N_CODES,I18nError@suaveplan/types workspace:^@suaveplan/error workspace:^@opentelemetry/api catalog:observabilitytslib catalog:buildzod catalog:forms- No dep on any other formatter package, on
@suaveplan/i18ncore, or on any backend package.
Out of scope
- Removing
formatting/list/frompackages/i18n/i18n/src/— happens in Wave 6. formatToPartsfor list output — follow-up; current monolith does not expose it.- Custom list separators — out of scope;
Intl.ListFormatalready 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 monolithlist.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 keypackages/i18n/i18n-formatter-list/src/formatter.spec-conformance.test.ts— every requirement scenario in this delta runs as an executable testpackages/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 namedi18n.format.list.duration_ms; counteri18n.format.cache_hit{formatter="list"}increments only on cache hitpackages/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/i18nslim core re-exportsListFormatterandformatListunder the back-compat root barrel. MANIFEST.mdandCLAUDE.mdupdated post-merge.