8.7 KiB
Capability Spec: i18n-formatter-collator (i18n-formatter-collator)
Package: @suaveplan/i18n-formatter-collator
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: CollatorFormatter class wrapping Intl.Collator
The package SHALL export a CollatorFormatter class that wraps Intl.Collator with construction-time Zod validation of options and compare(a: string, b: string, locale: string): -1 | 0 | 1 and sort(values: readonly string[], locale: string): string[] methods. Construction with malformed options SHALL throw I18nError(I18N.INVALID_FORMATTER_OPTIONS).
Scenario: compare with default options
- GIVEN
const f = new CollatorFormatter() - WHEN
f.compare("a", "b", "en-GB")is called - THEN the result equals
-1
Scenario: invalid options throw INVALID_FORMATTER_OPTIONS
- WHEN
new CollatorFormatter({ usage: "invalid" as never })is called - THEN the call throws
I18nErrorwith code"I18N.INVALID_FORMATTER_OPTIONS"
Verified by: packages/i18n/i18n-formatter-collator/src/formatter.test.ts::"CollatorFormatter compares with default options" AND packages/i18n/i18n-formatter-collator/src/formatter.test.ts::"CollatorFormatter rejects invalid options with INVALID_FORMATTER_OPTIONS"
Requirement: compareStrings standalone helper
The package SHALL export a compareStrings(a, b, locale, options?) standalone helper that returns -1, 0, or 1 per Intl.Collator.compare semantics.
Scenario: standalone compares with default options
- WHEN
compareStrings("apple", "banana", "en-GB")is called - THEN the result equals
-1
Scenario: standalone twins class output
- WHEN both
compareStrings(a, b, l, opts)andnew CollatorFormatter(opts).compare(a, b, l)are called for the same(a, b, l, opts) - THEN the results are identical
Verified by: packages/i18n/i18n-formatter-collator/src/formatter.test.ts::"compareStrings compares apple and banana" AND packages/i18n/i18n-formatter-collator/src/formatter.test.ts::"compareStrings output twins CollatorFormatter.compare output"
Requirement: formatCollation standalone helper returns sorted array
The package SHALL export a formatCollation(values, locale, options?) standalone helper that returns a sorted (immutable copy) array per the configured collator. The input array SHALL NOT be mutated.
Scenario: formatCollation sorts ascending
- WHEN
formatCollation(["c", "a", "b"], "en-GB")is called - THEN the result equals
["a", "b", "c"]
Scenario: formatCollation does not mutate the input
- GIVEN
const input = ["c", "a", "b"] - WHEN
formatCollation(input, "en-GB")is called - THEN
inputstill equals["c", "a", "b"]
Verified by: packages/i18n/i18n-formatter-collator/src/formatter.test.ts::"formatCollation sorts ascending" AND packages/i18n/i18n-formatter-collator/src/formatter.test.ts::"formatCollation does not mutate input array"
Requirement: Intl.Collator RangeError maps to UNSUPPORTED_LOCALE
The package SHALL catch every RangeError thrown by the Intl.Collator 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
compareStrings("a", "b", "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-collator/src/formatter.test.ts::"unsupported locale throws UNSUPPORTED_LOCALE with cause"
Requirement: Per-(locale, options-hash) cache
The package SHALL maintain an internal Map<string, Intl.Collator> keyed by ${locale}|${JSON.stringify(options)}. Repeated calls with identical (locale, options) SHALL reuse the same Intl.Collator instance. The package SHALL export a clearCollatorCache() test-only helper.
Scenario: identical options reuse the same Intl.Collator
- GIVEN the cache is empty
- WHEN
compareStrings("a", "b", "en-GB")is called twice in sequence - THEN only one
Intl.Collatorconstructor invocation occurs
Scenario: distinct options create distinct cache entries
- WHEN
compareStrings("a", "b", "en-GB", { sensitivity: "base" })andcompareStrings("a", "b", "en-GB", { sensitivity: "accent" })are called - THEN two distinct
Intl.Collatorinstances are created and cached
Verified by: packages/i18n/i18n-formatter-collator/src/formatter.test.ts::"identical options reuse cached Intl.Collator" AND packages/i18n/i18n-formatter-collator/src/formatter.test.ts::"distinct options create distinct cache entries"
Requirement: OTel histogram i18n.format.collator.duration_ms
The package SHALL emit an OTel histogram named exactly i18n.format.collator.duration_ms (unit ms) on every successful compareStrings or formatCollation call, tagged with locale, usage, and sensitivity.
Scenario: histogram emitted on every call
- GIVEN an OTel test exporter capturing histogram observations
- WHEN
compareStrings("a", "b", "en-GB", { sensitivity: "base", usage: "sort" })is called - THEN exactly one observation appears under
i18n.format.collator.duration_mswithlocale,usage, andsensitivityattributes
Verified by: packages/i18n/i18n-formatter-collator/src/observability.test.ts::"emits i18n.format.collator.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: "collator" 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
compareStrings("a", "b", "en-GB")is called twice in sequence - THEN the counter
i18n.format.cache_hit{formatter="collator"}shows exactly one increment
Verified by: packages/i18n/i18n-formatter-collator/src/observability.test.ts::"cache-hit counter increments only on second call with same options"
Requirement: Ergonomic helpers — case-insensitive, numeric-aware, sort
The package SHALL export three ergonomic helpers:
compareCaseInsensitive(a, b, locale)— pre-bindssensitivity: "accent"compareNumericAware(a, b, locale)— pre-bindsnumeric: truefor natural sort ("file2" < "file10")sortStrings(values, locale, options?)— returns sorted immutable copy
Scenario: compareCaseInsensitive treats a and A as equal
- WHEN
compareCaseInsensitive("a", "A", "en-GB")is called - THEN the result equals
0
Scenario: compareNumericAware orders file2 before file10
- WHEN
compareNumericAware("file2", "file10", "en-GB")is called - THEN the result equals
-1
Scenario: sortStrings sorts and returns immutable copy
- GIVEN
const input = ["c", "a", "b"] - WHEN
const out = sortStrings(input, "en-GB")is called - THEN
outequals["a", "b", "c"]ANDinputstill equals["c", "a", "b"]
Verified by: packages/i18n/i18n-formatter-collator/src/helpers.test.ts::"compareCaseInsensitive treats a and A as equal" AND packages/i18n/i18n-formatter-collator/src/helpers.test.ts::"compareNumericAware orders file2 before file10" AND packages/i18n/i18n-formatter-collator/src/helpers.test.ts::"sortStrings sorts and returns immutable copy"
Requirement: Subpath exports
The package SHALL declare subpath exports in package.json:
@suaveplan/i18n-formatter-collator— root barrel@suaveplan/i18n-formatter-collator/formatter—CollatorFormatterclass +compareStrings+formatCollation+ types + schema only@suaveplan/i18n-formatter-collator/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-collator/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-collator/src/index.test.ts::"package.json declares sideEffects false"