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

8.4 KiB

@suaveplan/i18n-formatter-collator — Intl.Collator wrapper (NEW formatter)

Package: @suaveplan/i18n-formatter-collator (NEW) Status: PROPOSED Type: feature (new universal-tier package — brand-new addition; no source in current monolith) 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 — closes the "two new formatters" gap from the original i18n-formatters change scope) Path: packages/i18n/i18n-formatter-collator/ Tier: universal

Why

The original i18n-formatters change folder (now archived under openspec/archive/2026-05-04-i18n-formatters/) listed Collator and Segmenter in its scope but the implementation never landed — only the four headline formatters (number, relative-time, list, date) shipped in the monolith. This change closes that gap as part of Wave 3 of the modular split.

Intl.Collator is the canonical cross-runtime API for locale-aware string comparison and sorting. Concrete uses across the monorepo:

  • @suaveplan/dedup today uses Jaro-Winkler / Levenshtein for similarity, but locale-aware exact-prefix sorting (e.g. customer names sorted in the user's locale) belongs on top of Intl.Collator, not raw string compare.
  • @suaveplan/react-data-table (Wave 3c-bound, downstream) needs locale-aware column sorting — Intl.Collator is the right primitive.
  • @suaveplan/i18n slim core (Wave 6) wires every formatter into the default FormatBridge. Collator slots in for the future {key, collator} ICU placeholder and for sort-key generation in catalog-key indexes.

Building this formatter from scratch (rather than waiting for a downstream consumer to inline new Intl.Collator(...)) means the same five hard rules every other Wave-3 formatter ships under (Zod-validated options, per-(locale, options-hash) cache, OTel histogram + cache-hit counter, structured error mapping, side-effect-free) apply uniformly across the family.

What changes

New package: @suaveplan/i18n-formatter-collator

  • ADDED: packages/i18n/i18n-formatter-collator/ package scaffold (package.json, tsconfig.json, vite.config.ts, README.md).
  • ADDED: src/formatter.ts — CollatorFormatter class wrapping Intl.Collator with construction-time Zod option validation; compareStrings(a, b, locale, options?) standalone (analogue of formatNumber); formatCollation(values, locale, options?) standalone returning the array sorted via the collator (analogue of formatList — the "format" verb in the family); clearCollatorCache test-only helper. Per-(locale, options-hash) cache key ${locale}|${JSON.stringify(options)}.
  • ADDED: src/formatter.types.ts — CollatorUsage ("sort" | "search"), CollatorSensitivity ("base" | "accent" | "case" | "variant"), CollatorCaseFirst ("upper" | "lower" | "false"), CollatorNumeric (boolean), CollatorFormatterOptions, CollatorFormatterCacheKey. Sourced from the ECMA-402 Intl.CollatorOptions surface.
  • ADDED: src/formatter.schemas.ts — CollatorFormatterOptionsSchema Zod schema validating every option per ECMA-402 (usage, sensitivity, ignorePunctuation, numeric, caseFirst, collation).
  • ADDED: src/helpers.ts — compareCaseInsensitive(a, b, locale) pre-binds sensitivity: "accent"; compareNumericAware(a, b, locale) pre-binds numeric: true (for natural sort "file2" < "file10"); sortStrings(values, locale, options?) returns the sorted array (immutable copy).
  • ADDED: src/observability.ts — local OTel histogram i18n.format.collator.duration_ms + counter i18n.format.cache_hit{formatter="collator"}.
  • ADDED: src/formatter.test.ts — covers default sort; case-insensitive; numeric-aware natural sort; usage "search"; class twins helper; Zod failure → INVALID_FORMATTER_OPTIONS; bogus locale → UNSUPPORTED_LOCALE.
  • ADDED: src/formatter.spec-conformance.test.ts — every requirement scenario in this delta runs as an executable test.
  • ADDED: src/helpers.test.ts — covers all three ergonomic helpers.
  • 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 — CollatorFormatter class + compareStrings + formatCollation + types + schema only
  • ./helpers — compareCaseInsensitive, compareNumericAware, sortStrings 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

  • Collation index generation (e.g. precomputed sort keys for database indexing) — out of scope; Intl.Collator does not expose getSortKey().
  • Custom collation tables (e.g. CLDR tailoring rules beyond what runtimes ship) — out of scope; matches Intl.Collator capabilities only.
  • Intl.Collator.supportedLocalesOf exposure — out of scope for Wave 3; can be added as a follow-up if downstream consumers need negotiation.
  • Removing this from the monolith — N/A (the monolith never shipped a collator).

Source mapping

Requirement Source file Source lines Notes
All requirements (no source in monolith) — Brand-new package; designed from scratch using @suaveplan/i18n-formatter-number (packages/i18n/i18n/src/formatting/number/number.ts) as the shape template

Requirements with no source line

Requirement Justification
CollatorFormatter class wrapping Intl.Collator Brand-new — closes the original i18n-formatters change scope; required by Wave 6 default FormatBridge for future {key, collator} ICU placeholder support
compareStrings, formatCollation standalones Brand-new — symmetric with formatNumber / formatList; compareStrings returns -1, 0, or 1; formatCollation returns the sorted array
Per-(locale, options-hash) cache Mandatory per Wave-3 formatter shape — every other formatter in the family ships this; same JSON-key pattern as NumberFormatter
Zod schema, INVALID_FORMATTER_OPTIONS, UNSUPPORTED_LOCALE mappings Mandatory per Wave-3 formatter shape
OTel histogram i18n.format.collator.duration_ms + cache-hit counter i18n.format.cache_hit{formatter="collator"} Mandatory per Wave-3 formatter shape
Three ergonomic helpers (compareCaseInsensitive, compareNumericAware, sortStrings) Symmetric with formatPercent / formatCompact / formatUnit for the number formatter; covers the highest-frequency real-world collation use cases

Verification approach

  • packages/i18n/i18n-formatter-collator/src/formatter.test.ts — compareStrings("a", "b", "en-GB") returns -1; case-sensitive vs case-insensitive divergence; numeric-aware sort orders "file2" before "file10"; class wrapper twins helper output; Zod failure → INVALID_FORMATTER_OPTIONS; bogus locale → UNSUPPORTED_LOCALE; cache reuse on identical key
  • packages/i18n/i18n-formatter-collator/src/formatter.spec-conformance.test.ts — every requirement scenario runs as an executable test
  • packages/i18n/i18n-formatter-collator/src/helpers.test.ts — compareCaseInsensitive("a", "A", "en-GB") returns 0; compareNumericAware("file2", "file10", "en-GB") returns -1; sortStrings(["c", "a", "b"], "en-GB") returns ["a", "b", "c"] (immutable copy verified)
  • packages/i18n/i18n-formatter-collator/src/observability.test.ts — histogram named i18n.format.collator.duration_ms; counter i18n.format.cache_hit{formatter="collator"} increments only on second call with same (locale, options)
  • packages/i18n/i18n-formatter-collator/src/index.test.ts — every documented public export resolves; subpaths resolve

Impact

  • New universal-tier package — joins the rest of the i18n family.
  • Indirect consumer in Wave 6: @suaveplan/i18n slim core re-exports CollatorFormatter and formatCollation under the back-compat root barrel (per design doc §8.1 — formatCollation is the listed re-export).
  • Closes a gap from the original i18n-formatters change scope (now archived under openspec/archive/2026-05-04-i18n-formatters/).
  • MANIFEST.md and CLAUDE.md updated post-merge.