Files
2026-05-27 23:29:53 +01:00

8.7 KiB

@suaveplan/i18n-formatter-segmenter — Intl.Segmenter wrapper (NEW formatter)

Package: @suaveplan/i18n-formatter-segmenter (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-segmenter/ 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 the second half of that gap as part of Wave 3 of the modular split.

Intl.Segmenter is the canonical cross-runtime API for locale-aware text segmentation by grapheme, word, or sentence. Concrete uses across the monorepo:

  • @suaveplan/security-codes today scans for sensitive substrings; word-boundary-aware redaction (e.g. masking only complete words) belongs on top of Intl.Segmenter.
  • @suaveplan/react-rich-text (Wave 3c-bound, downstream) needs grapheme-aware cursor movement (single emoji = single grapheme even across multiple code units) — Intl.Segmenter("en-GB", { granularity: "grapheme" }) is the right primitive.
  • @suaveplan/i18n-icu-parser plural-rule selection at glyph boundaries benefits from grapheme segmentation (today the parser counts code units, which is correct for digits but wrong for emoji-prefixed numbers in some experimental ICU extensions — out of scope for this package, in scope for a follow-up).

Building this formatter from scratch follows the same Wave-3 formatter shape as every sibling package (Zod-validated options, per-(locale, options-hash) cache, OTel histogram + cache-hit counter, structured error mapping, side-effect-free).

What changes

New package: @suaveplan/i18n-formatter-segmenter

  • ADDED: packages/i18n/i18n-formatter-segmenter/ package scaffold (package.json, tsconfig.json, vite.config.ts, README.md).
  • ADDED: src/formatter.ts — SegmenterFormatter class wrapping Intl.Segmenter with construction-time Zod option validation; segmentText(input, locale, options?) standalone (returns string[] of segments — analogue of formatList); formatSegments(input, locale, options?) standalone (returns Intl.SegmentData[] with full index / segment / isWordLike metadata for each segment); clearSegmenterCache test-only helper. Per-(locale, granularity) cache key ${locale}|${granularity}.
  • ADDED: src/formatter.types.ts — SegmenterGranularity ("grapheme" | "word" | "sentence"), SegmenterFormatterOptions ({ readonly granularity?: SegmenterGranularity }), SegmentData (re-export of Intl.SegmentData with stable shape), SegmenterFormatterCacheKey.
  • ADDED: src/formatter.schemas.ts — SegmenterFormatterOptionsSchema Zod schema validating granularity ∈ {grapheme, word, sentence}.
  • ADDED: src/helpers.ts — countGraphemes(input, locale) (returns the grapheme count for emoji-aware character counting); splitWords(input, locale) returns the word-segments only (filters isWordLike: true); splitSentences(input, locale) returns sentence-granularity segments.
  • ADDED: src/observability.ts — local OTel histogram i18n.format.segmenter.duration_ms + counter i18n.format.cache_hit{formatter="segmenter"}.
  • ADDED: src/formatter.test.ts — covers grapheme / word / sentence granularity; emoji handled as single grapheme; 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 (especially countGraphemes on emoji input).
  • 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 — SegmenterFormatter class + segmentText + formatSegments + types + schema only
  • ./helpers — countGraphemes, splitWords, splitSentences 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

  • Custom segmentation rules (e.g. ICU tailoring rules) — out of scope; matches Intl.Segmenter capabilities only.
  • Pre-Intl.Segmenter runtime polyfill — out of scope; Intl.Segmenter shipped in Node 16+, Bun, and every major browser. Older runtimes are not supported.
  • SegmentIterator streaming over ReadableStream — out of scope; Intl.Segmenter.segment() accepts only a string. Streaming text segmentation is a follow-up (likely a separate @suaveplan/i18n-formatter-segmenter-stream package).
  • Removing this from the monolith — N/A (the monolith never shipped a segmenter).

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
SegmenterFormatter class wrapping Intl.Segmenter Brand-new — closes the original i18n-formatters change scope; required by Wave 6 default FormatBridge for future grapheme-aware ICU extensions
segmentText, formatSegments standalones Brand-new — segmentText returns string[] (most consumer-friendly for splitting); formatSegments returns SegmentData[] (preserves index and isWordLike metadata)
Per-(locale, granularity) cache Mandatory per Wave-3 formatter shape — every other formatter in the family ships this; smaller cache key (only granularity) than NumberFormatter because Intl.Segmenter has only one option
Zod schema, INVALID_FORMATTER_OPTIONS, UNSUPPORTED_LOCALE mappings Mandatory per Wave-3 formatter shape
OTel histogram i18n.format.segmenter.duration_ms + cache-hit counter i18n.format.cache_hit{formatter="segmenter"} Mandatory per Wave-3 formatter shape
Three ergonomic helpers (countGraphemes, splitWords, splitSentences) Symmetric with formatPercent / formatCompact / formatUnit for the number formatter; covers the highest-frequency real-world segmentation use cases

Verification approach

  • packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts — segmentText("hello world", "en-GB", { granularity: "word" }) returns ["hello", " ", "world"] (or platform-equivalent); grapheme granularity treats "👨‍👩‍👧" as a single segment; sentence granularity splits on ./?/!; class wrapper twins helper output; Zod failure → INVALID_FORMATTER_OPTIONS; bogus locale → UNSUPPORTED_LOCALE; cache reuse on identical key
  • packages/i18n/i18n-formatter-segmenter/src/formatter.spec-conformance.test.ts — every requirement scenario runs as an executable test
  • packages/i18n/i18n-formatter-segmenter/src/helpers.test.ts — countGraphemes("👨‍👩‍👧", "en-GB") returns 1; countGraphemes("hello", "en-GB") returns 5; splitWords("hello, world!", "en-GB") returns ["hello", "world"] (filters non-word-like segments); splitSentences("Hi. How are you?", "en-GB") returns two sentence segments
  • packages/i18n/i18n-formatter-segmenter/src/observability.test.ts — histogram named i18n.format.segmenter.duration_ms; counter i18n.format.cache_hit{formatter="segmenter"} increments only on second call with same (locale, granularity)
  • packages/i18n/i18n-formatter-segmenter/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 SegmenterFormatter and formatSegments under the back-compat root barrel (per design doc §8.1 — formatSegments 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.