9.3 KiB
Capability Spec: i18n-formatter-segmenter (i18n-formatter-segmenter)
Package: @suaveplan/i18n-formatter-segmenter
Status: ACTIVE
Every requirement below carries a
**Verified by:**line at authoring time. (Seeopenspec/AGENTS.mdSection 13.)NEW formatter — no source in current monolith. Closes the gap from the original
i18n-formatterschange scope.
Requirements
Requirement: SegmenterFormatter class wrapping Intl.Segmenter
The package SHALL export a SegmenterFormatter class that wraps Intl.Segmenter with construction-time Zod validation of options and segment(input: string, locale: string): string[] and segmentWithMetadata(input: string, locale: string): SegmentData[] methods. Construction with malformed options SHALL throw I18nError(I18N.INVALID_FORMATTER_OPTIONS).
Scenario: segment with default options (grapheme)
- GIVEN
const f = new SegmenterFormatter() - WHEN
f.segment("ab", "en-GB")is called - THEN the result equals
["a", "b"]
Scenario: invalid options throw INVALID_FORMATTER_OPTIONS
- WHEN
new SegmenterFormatter({ granularity: "invalid" as never })is called - THEN the call throws
I18nErrorwith code"I18N.INVALID_FORMATTER_OPTIONS"
Verified by: packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"SegmenterFormatter segments with default options" AND packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"SegmenterFormatter rejects invalid options with INVALID_FORMATTER_OPTIONS"
Requirement: segmentText standalone helper returns string[]
The package SHALL export a segmentText(input, locale, options?) standalone helper that returns the segments as a plain string[].
Scenario: word granularity returns word segments
- WHEN
segmentText("hello world", "en-GB", { granularity: "word" })is called - THEN the result includes both
"hello"and"world"as segments
Scenario: standalone twins class output
- WHEN both
segmentText(input, l, opts)andnew SegmenterFormatter(opts).segment(input, l)are called for the same(input, l, opts) - THEN the results are identical
Verified by: packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"segmentText word granularity returns word segments" AND packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"segmentText output twins SegmenterFormatter.segment output"
Requirement: formatSegments standalone helper returns SegmentData[]
The package SHALL export a formatSegments(input, locale, options?) standalone helper that returns SegmentData[] with each entry preserving { segment, index, isWordLike } metadata per Intl.SegmentData.
Scenario: formatSegments preserves index and isWordLike
- WHEN
formatSegments("hi!", "en-GB", { granularity: "word" })is called - THEN the first entry has
segment: "hi",index: 0,isWordLike: trueAND a subsequent entry hassegment: "!",isWordLike: false
Verified by: packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"formatSegments preserves index and isWordLike metadata"
Requirement: Grapheme granularity treats compound emoji as a single segment
The package SHALL respect ECMA-402 Intl.Segmenter semantics: a compound emoji (e.g. "👨👩👧", the family-of-three ZWJ sequence) SHALL be returned as a single segment under granularity: "grapheme".
Scenario: family emoji is a single grapheme
- WHEN
segmentText("👨👩👧", "en-GB", { granularity: "grapheme" })is called - THEN the result has length
1
Verified by: packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"compound emoji is a single grapheme segment"
Requirement: Intl.Segmenter RangeError maps to UNSUPPORTED_LOCALE
The package SHALL catch every RangeError thrown by the Intl.Segmenter constructor and re-throw it as I18nError(I18N.UNSUPPORTED_LOCALE) with the original error attached as cause and the locale string in context.
Scenario: bogus locale throws UNSUPPORTED_LOCALE
- WHEN
segmentText("a", "xx-bogus-tag-!!!")is called - THEN the call throws
I18nErrorwith code"I18N.UNSUPPORTED_LOCALE"AND the error'scauseis the originalRangeError
Verified by: packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"unsupported locale throws UNSUPPORTED_LOCALE with cause"
Requirement: Per-(locale, granularity) cache
The package SHALL maintain an internal Map<string, Intl.Segmenter> keyed by ${locale}|${granularity}. Repeated calls with identical (locale, granularity) SHALL reuse the same Intl.Segmenter instance. The package SHALL export a clearSegmenterCache() test-only helper.
Scenario: identical key reuses the same Intl.Segmenter
- GIVEN the cache is empty
- WHEN
segmentText("a", "en-GB")is called twice in sequence - THEN only one
Intl.Segmenterconstructor invocation occurs
Scenario: distinct granularity creates distinct cache entries
- WHEN
segmentText("a", "en-GB", { granularity: "grapheme" })andsegmentText("a", "en-GB", { granularity: "word" })are called - THEN two distinct
Intl.Segmenterinstances are created and cached
Verified by: packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"identical key reuses cached Intl.Segmenter" AND packages/i18n/i18n-formatter-segmenter/src/formatter.test.ts::"distinct granularity creates distinct cache entries"
Requirement: OTel histogram i18n.format.segmenter.duration_ms
The package SHALL emit an OTel histogram named exactly i18n.format.segmenter.duration_ms (unit ms) on every successful segmentText or formatSegments call, tagged with locale and granularity.
Scenario: histogram emitted on every call
- GIVEN an OTel test exporter capturing histogram observations
- WHEN
segmentText("hello", "en-GB", { granularity: "word" })is called - THEN exactly one observation appears under
i18n.format.segmenter.duration_mswithlocaleandgranularityattributes
Verified by: packages/i18n/i18n-formatter-segmenter/src/observability.test.ts::"emits i18n.format.segmenter.duration_ms histogram on every call"
Requirement: OTel cache-hit counter i18n.format.cache_hit
The package SHALL emit an OTel counter i18n.format.cache_hit tagged with formatter: "segmenter" on every cache hit. The counter SHALL NOT increment on the first (cache-miss) call.
Scenario: cache-hit counter increments only on second call
- GIVEN an OTel test exporter capturing counter observations and an empty cache
- WHEN
segmentText("a", "en-GB")is called twice in sequence - THEN the counter
i18n.format.cache_hit{formatter="segmenter"}shows exactly one increment
Verified by: packages/i18n/i18n-formatter-segmenter/src/observability.test.ts::"cache-hit counter increments only on second call with same options"
Requirement: Ergonomic helpers — countGraphemes, splitWords, splitSentences
The package SHALL export three ergonomic helpers:
countGraphemes(input, locale)— returns the grapheme count (emoji-aware)splitWords(input, locale)— returns word-segments only (filtersisWordLike: true)splitSentences(input, locale)— returns sentence-granularity segments
Scenario: countGraphemes treats compound emoji as one
- WHEN
countGraphemes("👨👩👧", "en-GB")is called - THEN the result equals
1
Scenario: countGraphemes counts ASCII characters
- WHEN
countGraphemes("hello", "en-GB")is called - THEN the result equals
5
Scenario: splitWords filters non-word-like segments
- WHEN
splitWords("hello, world!", "en-GB")is called - THEN the result equals
["hello", "world"]
Scenario: splitSentences returns sentence segments
- WHEN
splitSentences("Hi. How are you?", "en-GB")is called - THEN the result has length
2
Verified by: packages/i18n/i18n-formatter-segmenter/src/helpers.test.ts::"countGraphemes treats compound emoji as one" AND packages/i18n/i18n-formatter-segmenter/src/helpers.test.ts::"countGraphemes counts ASCII characters" AND packages/i18n/i18n-formatter-segmenter/src/helpers.test.ts::"splitWords filters non-word-like segments" AND packages/i18n/i18n-formatter-segmenter/src/helpers.test.ts::"splitSentences returns sentence segments"
Requirement: Subpath exports
The package SHALL declare subpath exports in package.json:
@suaveplan/i18n-formatter-segmenter— root barrel@suaveplan/i18n-formatter-segmenter/formatter—SegmenterFormatterclass +segmentText+formatSegments+ types + schema only@suaveplan/i18n-formatter-segmenter/helpers— three ergonomic helpers only
Scenario: every documented subpath resolves
- WHEN every documented subpath is imported in a consumer test fixture
- THEN every import resolves with no
Module not founderrors
Verified by: packages/i18n/i18n-formatter-segmenter/src/index.test.ts::"every documented subpath export resolves"
Requirement: Side-effect-free package
The package SHALL declare "sideEffects": false in package.json.
Scenario: package.json declares sideEffects false
- WHEN
package.jsonis parsed - THEN the
sideEffectsfield equalsfalse
Verified by: packages/i18n/i18n-formatter-segmenter/src/index.test.ts::"package.json declares sideEffects false"