7.1 KiB
Capability Spec: i18n-formatter-list (i18n-formatter-list)
Package: @suaveplan/i18n-formatter-list
Status: ACTIVE
Every requirement below carries a
**Verified by:**line at authoring time. (Seeopenspec/AGENTS.mdSection 13.)
Requirements
Requirement: ListFormatter class wrapping Intl.ListFormat
The package SHALL export a ListFormatter class that wraps Intl.ListFormat with construction-time Zod validation of options and a format(values: readonly string[], locale: string): string method. Construction with malformed options SHALL throw I18nError(I18N.INVALID_FORMATTER_OPTIONS).
Scenario: format with default options (long conjunction)
- GIVEN
const f = new ListFormatter() - WHEN
f.format(["a", "b", "c"], "en-GB")is called - THEN the result equals
"a, b, and c"
Scenario: invalid options throw INVALID_FORMATTER_OPTIONS
- WHEN
new ListFormatter({ type: "invalid" as never })is called - THEN the call throws
I18nErrorwith code"I18N.INVALID_FORMATTER_OPTIONS"
Verified by: packages/i18n/i18n-formatter-list/src/formatter.test.ts::"ListFormatter formats with default options" AND packages/i18n/i18n-formatter-list/src/formatter.test.ts::"ListFormatter rejects invalid options with INVALID_FORMATTER_OPTIONS"
Requirement: formatList standalone helper
The package SHALL export a formatList(values, locale, options?) standalone helper that delivers the same behaviour as ListFormatter.format without requiring class construction. Defaults: style: "long", type: "conjunction".
Scenario: standalone helper formats with disjunction type
- WHEN
formatList(["x", "y"], "en-GB", { type: "disjunction" })is called - THEN the result equals
"x or y"
Scenario: standalone helper twins class output
- WHEN both
formatList(v, l, opts)andnew ListFormatter(opts).format(v, l)are called for the same(v, l, opts) - THEN the results are identical
Verified by: packages/i18n/i18n-formatter-list/src/formatter.test.ts::"formatList formats disjunction" AND packages/i18n/i18n-formatter-list/src/formatter.test.ts::"formatList output twins ListFormatter.format output"
Requirement: Intl.ListFormat RangeError maps to UNSUPPORTED_LOCALE
The package SHALL catch every RangeError thrown by the Intl.ListFormat 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
formatList(["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-list/src/formatter.test.ts::"unsupported locale throws UNSUPPORTED_LOCALE with cause"
Requirement: Per-(locale, style, type) cache
The package SHALL maintain an internal Map<string, Intl.ListFormat> keyed by ${locale}|${style}|${type}. Repeated calls with identical (locale, style, type) SHALL reuse the same Intl.ListFormat instance. The package SHALL export a clearListFormatterCache() test-only helper.
Scenario: identical key reuses the same Intl.ListFormat
- GIVEN the cache is empty
- WHEN
formatList(["a"], "en-GB")is called twice in sequence - THEN only one
Intl.ListFormatconstructor invocation occurs
Scenario: distinct type creates distinct cache entries
- WHEN
formatList(["a"], "en-GB", { type: "conjunction" })andformatList(["a"], "en-GB", { type: "disjunction" })are called - THEN two distinct
Intl.ListFormatinstances are created and cached
Verified by: packages/i18n/i18n-formatter-list/src/formatter.test.ts::"identical key reuses cached Intl.ListFormat" AND packages/i18n/i18n-formatter-list/src/formatter.test.ts::"distinct type creates distinct cache entries"
Requirement: OTel histogram i18n.format.list.duration_ms
The package SHALL emit an OTel histogram named exactly i18n.format.list.duration_ms (unit ms) on every successful formatList call, tagged with locale, style, and type.
Scenario: histogram emitted on every call
- GIVEN an OTel test exporter capturing histogram observations
- WHEN
formatList(["a", "b"], "en-GB", { style: "long", type: "conjunction" })is called - THEN exactly one observation appears under
i18n.format.list.duration_mswithlocale,style, andtypeattributes
Verified by: packages/i18n/i18n-formatter-list/src/observability.test.ts::"emits i18n.format.list.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: "list" 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
formatList(["a"], "en-GB")is called twice in sequence - THEN the counter
i18n.format.cache_hit{formatter="list"}shows exactly one increment
Verified by: packages/i18n/i18n-formatter-list/src/observability.test.ts::"cache-hit counter increments only on second call with same options"
Requirement: Ergonomic helpers — formatConjunction, formatDisjunction
The package SHALL export formatConjunction(values, locale, style?) and formatDisjunction(values, locale, style?) ergonomic shortcuts pre-binding formatList with the corresponding type.
Scenario: formatConjunction renders comma + and
- WHEN
formatConjunction(["a", "b", "c"], "en-GB")is called - THEN the result equals
"a, b, and c"
Scenario: formatDisjunction renders or
- WHEN
formatDisjunction(["x", "y"], "en-GB")is called - THEN the result equals
"x or y"
Verified by: packages/i18n/i18n-formatter-list/src/helpers.test.ts::"formatConjunction renders a, b, and c" AND packages/i18n/i18n-formatter-list/src/helpers.test.ts::"formatDisjunction renders x or y"
Requirement: Subpath exports
The package SHALL declare subpath exports in package.json so consumers can import only the surfaces they need:
@suaveplan/i18n-formatter-list— root barrel@suaveplan/i18n-formatter-list/formatter—ListFormatterclass +formatList+ types + schema only@suaveplan/i18n-formatter-list/helpers—formatConjunction,formatDisjunctiononly
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-list/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-list/src/index.test.ts::"package.json declares sideEffects false"