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

7.3 KiB
Raw Blame History

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