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

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. (See openspec/AGENTS.md Section 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 I18nError with 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) and new 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 I18nError with code "I18N.UNSUPPORTED_LOCALE" AND the error's cause is the original RangeError

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.ListFormat constructor invocation occurs

Scenario: distinct type creates distinct cache entries

  • WHEN formatList(["a"], "en-GB", { type: "conjunction" }) and formatList(["a"], "en-GB", { type: "disjunction" }) are called
  • THEN two distinct Intl.ListFormat instances 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_ms with locale, style, and type attributes

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 — ListFormatter class + formatList + types + schema only
  • @suaveplan/i18n-formatter-list/helpers — formatConjunction, formatDisjunction 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 found errors

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.json is parsed
  • THEN the sideEffects field equals false

Verified by: packages/i18n/i18n-formatter-list/src/index.test.ts::"package.json declares sideEffects false"