11 KiB
Capability Spec: i18n-shared (i18n-shared)
Package: @suaveplan/i18n-shared
Status: ACTIVE
Every requirement below carries a
**Verified by:**line at authoring time. (Seeopenspec/AGENTS.mdSection 13.)
Requirements
Requirement: BCP-47 locale tag map
The package SHALL export LOCALE_BCP47_MAP: Record<string, string> mapping short ISO 639-1 codes (and common mixed-case forms) to canonical BCP-47 tags. Lookups for unknown codes SHALL pass through unchanged at the consumer level (the constant is a plain Record; consumers handle the miss).
Scenario: short code maps to canonical BCP-47
- WHEN
LOCALE_BCP47_MAP["en"]is looked up - THEN the value equals
"en-GB"
Scenario: mixed-case code maps to canonical form
- WHEN
LOCALE_BCP47_MAP["zh-cn"]is looked up - THEN the value equals
"zh-CN"
Verified by: packages/i18n/i18n-shared/src/constants/locale-bcp47-map.test.ts::"short code en maps to en-GB" AND packages/i18n/i18n-shared/src/constants/locale-bcp47-map.test.ts::"mixed-case zh-cn maps to zh-CN"
Requirement: RTL locale set and tuple
The package SHALL export RTL_LOCALES: ReadonlySet<string> containing the seven ISO 639-1 codes for right-to-left scripts: ar, he, fa, ur, ps, sd, yi. The package SHALL also export RTL_LOCALE_LIST as a tree-shake-friendly readonly tuple of the same codes.
Scenario: ar is RTL
- WHEN
RTL_LOCALES.has("ar")is evaluated - THEN the result is
true
Scenario: en is not RTL
- WHEN
RTL_LOCALES.has("en")is evaluated - THEN the result is
false
Scenario: list and set agree on size
- WHEN
RTL_LOCALE_LIST.lengthis compared toRTL_LOCALES.size - THEN the values are equal
Verified by: packages/i18n/i18n-shared/src/constants/rtl-locales.test.ts::"ar is RTL" AND packages/i18n/i18n-shared/src/constants/rtl-locales.test.ts::"en is not RTL" AND packages/i18n/i18n-shared/src/constants/rtl-locales.test.ts::"RTL_LOCALE_LIST length matches RTL_LOCALES size"
Requirement: Locale descriptors with name, native name, and direction
The package SHALL export LOCALE_DESCRIPTORS as a Record<string, { name: string; nativeName: string; direction: "ltr" | "rtl" }> covering every entry in DEFAULT_LOCALES. Each descriptor's direction SHALL match the locale's membership in RTL_LOCALES.
Scenario: every default locale has a descriptor
- WHEN every code in
DEFAULT_LOCALESis looked up inLOCALE_DESCRIPTORS - THEN every lookup returns a defined descriptor
Scenario: Arabic descriptor is RTL
- WHEN
LOCALE_DESCRIPTORS.ar.directionis read - THEN the value equals
"rtl"
Scenario: native names are non-empty
- WHEN every descriptor's
nativeNameis checked - THEN every value is a non-empty string
Verified by: packages/i18n/i18n-shared/src/constants/locale-descriptors.test.ts::"every DEFAULT_LOCALES code has a descriptor" AND packages/i18n/i18n-shared/src/constants/locale-descriptors.test.ts::"ar descriptor direction is rtl" AND packages/i18n/i18n-shared/src/constants/locale-descriptors.test.ts::"every descriptor nativeName is non-empty"
Requirement: Default interpolation defaults
The package SHALL export DEFAULT_INTERPOLATION as a frozen object with prefix: "{{", suffix: "}}", and maxDepth: 10. The shape SHALL be as const so consumers can narrow at the type level.
Scenario: default prefix and suffix
- WHEN
DEFAULT_INTERPOLATION.prefixandDEFAULT_INTERPOLATION.suffixare read - THEN the values equal
"{{"and"}}"respectively
Scenario: default max depth
- WHEN
DEFAULT_INTERPOLATION.maxDepthis read - THEN the value equals
10
Scenario: type is narrowed via as const
- WHEN
expectTypeOf<typeof DEFAULT_INTERPOLATION.prefix>().toEqualTypeOf<"{{">()runs - THEN the type-level check passes
Verified by: packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts::"default prefix is {{" AND packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts::"default suffix is }}" AND packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts::"default maxDepth is 10" AND packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts::"DEFAULT_INTERPOLATION is narrowed via as const"
Requirement: I18N error code registry
The package SHALL export I18N_CODES constructed via defineCodes("I18N", [...] as const) from @suaveplan/error, containing the following 18 codes:
- Parser / interpolation / pluralization:
MISSING_KEY,INVALID_LOCALE,INVALID_OPTIONS,LOADER_FAILED,INTERPOLATION_ERROR,PLURAL_RULE_NOT_FOUND,NESTING_DEPTH_EXCEEDED - Backends:
BACKEND_READ_FAILED,BACKEND_WRITE_FAILED,BACKEND_NOT_REGISTERED,BACKEND_LOCALE_NOT_AVAILABLE,BACKEND_TIMEOUT,CHAINED_BACKEND_ALL_FAILED - Formatters:
INVALID_FORMATTER_OPTIONS,UNSUPPORTED_LOCALE,FORMATTER_CACHE_OVERFLOW - Detector:
DETECTOR_CHAIN_EXHAUSTED - Plugin / core:
PLUGIN_ALREADY_INSTALLED
Every code SHALL be addressable as I18N_CODES.<NAME> and SHALL produce a string literal of the form "I18N.<NAME>". The package SHALL also export I18nErrorCode as the discriminant union of all 18 string literals.
Scenario: every documented code is in the registry
- WHEN every name listed in this requirement is read from
I18N_CODES - THEN every read returns a defined value
Scenario: code values follow the I18N. prefix
- WHEN
I18N_CODES.MISSING_KEYis read - THEN the value equals
"I18N.MISSING_KEY"
Scenario: I18nErrorCode union has cardinality 18
- WHEN the type-level cardinality of
I18nErrorCodeis asserted - THEN the union covers exactly 18 string literals
Verified by: packages/i18n/i18n-shared/src/errors/error-codes.test.ts::"every documented code is in I18N_CODES" AND packages/i18n/i18n-shared/src/errors/error-codes.test.ts::"code values are namespaced under I18N." AND packages/i18n/i18n-shared/src/errors/error-codes.test.ts::"I18nErrorCode union has cardinality 18"
Requirement: I18nError class
The package SHALL export I18nError extends SuaveplanError<I18nErrorCode> from @suaveplan/error. The class SHALL be constructible from any code in I18N_CODES and SHALL satisfy instanceof I18nError and instanceof SuaveplanError. The class SHALL preserve the cause chain when constructed with { cause }. The class SHALL NOT carry any biome / linter suppression comments per CLAUDE.md §9.
Scenario: construction with each code
- WHEN
new I18nError(code, "test")is called for every code inI18N_CODES - THEN every construction succeeds
Scenario: instanceof discrimination
- WHEN an
I18nErroris caught - THEN
instanceof I18nErrorreturnstrueANDinstanceof SuaveplanErrorreturnstrue
Scenario: cause preservation
- WHEN
new I18nError(I18N_CODES.LOADER_FAILED, "k", { cause: new Error("network") })is constructed - THEN the resulting error's
.cause.messageequals"network"
Scenario: no biome suppressions in source
- WHEN the file
src/errors/i18n-error.tsis grepped forbiome-ignore - THEN zero matches are found
Verified by: packages/i18n/i18n-shared/src/errors/i18n-error.test.ts::"constructs with every code in I18N_CODES" AND packages/i18n/i18n-shared/src/errors/i18n-error.test.ts::"instanceof I18nError and SuaveplanError" AND packages/i18n/i18n-shared/src/errors/i18n-error.test.ts::"preserves cause chain" AND packages/i18n/i18n-shared/src/errors/i18n-error.test.ts::"source contains no biome-ignore suppressions"
Requirement: Subpath exports for tree-shaking
The package SHALL declare subpath exports in package.json so consumers can import individual constants without pulling the entire bundle. The following subpaths SHALL resolve:
@suaveplan/i18n-shared— root barrel@suaveplan/i18n-shared/constants— all constants@suaveplan/i18n-shared/constants/locale-bcp47-map—LOCALE_BCP47_MAPonly@suaveplan/i18n-shared/constants/rtl-locales—RTL_LOCALESandRTL_LOCALE_LISTonly@suaveplan/i18n-shared/constants/locale-descriptors—LOCALE_DESCRIPTORS,DEFAULT_LOCALES,DefaultLocaleonly@suaveplan/i18n-shared/constants/interpolation-defaults—DEFAULT_INTERPOLATIONonly@suaveplan/i18n-shared/errors—I18N_CODES,I18nError,I18nErrorCode@suaveplan/i18n-shared/testing—runBackendConformance,ConformanceContext,ConformanceFactory
Scenario: every subpath resolves at type-check time
- 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-shared/src/index.test.ts::"every documented subpath export resolves"
Requirement: Side-effect-free package
The package SHALL declare "sideEffects": false in package.json so bundlers can tree-shake unused exports.
Scenario: package.json declares sideEffects false
- WHEN
package.jsonis parsed - THEN the
sideEffectsfield equalsfalse
Verified by: packages/i18n/i18n-shared/src/byte-size.test.ts::"package.json declares sideEffects false"
Requirement: Backend conformance harness
The package SHALL expose a runBackendConformance(label, makeFactory) function under the @suaveplan/i18n-shared/testing subpath. The harness SHALL exercise the universal Backend interface from @suaveplan/types (name + capabilities surface, read-of-absent → I18nError("I18N.BACKEND_LOCALE_NOT_AVAILABLE"), optional readMulti merge, optional save round-trip, optional lastModified monotonicity) and SHALL gate each capability-bearing assertion on both the structural presence of the optional method AND the matching capabilities.canX flag, so backends with partial implementations and read-only backends both pass without modification. The harness SHALL import describe / it / expect exclusively from @suaveplan/testing/runner so it executes identically under Vitest and Bun's test runner.
Scenario: harness exports a callable runner
- WHEN
runBackendConformanceis imported from@suaveplan/i18n-shared/testing - THEN the value is a function with arity 2
Scenario: harness covers a full-featured backend
- GIVEN an in-memory backend implementing every capability
- WHEN
runBackendConformance("MockFullBackend", factory)is called - THEN every assertion in the suite passes
Scenario: harness gates capabilities for read-only backends
- GIVEN a backend with
canReadMulti/canSave/canTrackModificationallfalse - WHEN the harness runs
- THEN the read-multi / save / last-modified assertions early-return without failing
Verified by: packages/i18n/i18n-shared/src/testing/conformance.test.ts::"is a callable function" AND the full Backend conformance: MockFullBackend block AND the full Backend conformance: MockReadOnlyBackend block AND the full Backend conformance: MockCapabilityMismatchBackend block