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-codestoday scans for sensitive substrings; word-boundary-aware redaction (e.g. masking only complete words) belongs on top ofIntl.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-parserplural-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—SegmenterFormatterclass wrappingIntl.Segmenterwith construction-time Zod option validation;segmentText(input, locale, options?)standalone (returnsstring[]of segments — analogue offormatList);formatSegments(input, locale, options?)standalone (returnsIntl.SegmentData[]with fullindex/segment/isWordLikemetadata for each segment);clearSegmenterCachetest-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 ofIntl.SegmentDatawith stable shape),SegmenterFormatterCacheKey. - ADDED:
src/formatter.schemas.ts—SegmenterFormatterOptionsSchemaZod schema validatinggranularity ∈ {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 (filtersisWordLike: true);splitSentences(input, locale)returns sentence-granularity segments. - ADDED:
src/observability.ts— local OTel histogrami18n.format.segmenter.duration_ms+ counteri18n.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 (especiallycountGraphemeson 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—SegmenterFormatterclass +segmentText+formatSegments+ types + schema only./helpers—countGraphemes,splitWords,splitSentencesonly
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
- Custom segmentation rules (e.g. ICU
tailoringrules) — out of scope; matchesIntl.Segmentercapabilities only. - Pre-
Intl.Segmenterruntime polyfill — out of scope;Intl.Segmentershipped in Node 16+, Bun, and every major browser. Older runtimes are not supported. SegmentIteratorstreaming overReadableStream— out of scope;Intl.Segmenter.segment()accepts only a string. Streaming text segmentation is a follow-up (likely a separate@suaveplan/i18n-formatter-segmenter-streampackage).- 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 keypackages/i18n/i18n-formatter-segmenter/src/formatter.spec-conformance.test.ts— every requirement scenario runs as an executable testpackages/i18n/i18n-formatter-segmenter/src/helpers.test.ts—countGraphemes("👨👩👧", "en-GB")returns1;countGraphemes("hello", "en-GB")returns5;splitWords("hello, world!", "en-GB")returns["hello", "world"](filters non-word-like segments);splitSentences("Hi. How are you?", "en-GB")returns two sentence segmentspackages/i18n/i18n-formatter-segmenter/src/observability.test.ts— histogram namedi18n.format.segmenter.duration_ms; counteri18n.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/i18nslim core re-exportsSegmenterFormatterandformatSegmentsunder the back-compat root barrel (per design doc §8.1 —formatSegmentsis the listed re-export). - Closes a gap from the original
i18n-formatterschange scope (now archived underopenspec/archive/2026-05-04-i18n-formatters/). MANIFEST.mdandCLAUDE.mdupdated post-merge.