15 KiB
Capability Spec Delta: i18n (i18n-core-slim, Wave 6)
Package: @suaveplan/i18n
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. (Seeopenspec/AGENTS.mdSection 13.)
REMOVED Requirements
The following requirements MOVE to sibling packages. The capability still holds at the family level but is no longer hosted by this package directly. Each removal carries a **Now hosted by:** pointer so the conformance gate can find the new home.
Requirement: BCP-47 locale tag map exposed by @suaveplan/i18n
REMOVED. The LOCALE_BCP47_MAP constant is no longer hosted by @suaveplan/i18n directly; it is re-exported from @suaveplan/i18n-shared/constants/locale-bcp47-map via the back-compat root barrel.
Now hosted by: @suaveplan/i18n-shared (capability spec: openspec/specs/i18n-shared/spec.md)
Requirement: RTL locale set exposed by @suaveplan/i18n
REMOVED. The RTL_LOCALES and RTL_LOCALE_LIST constants move to @suaveplan/i18n-shared. Re-exported via root barrel for back-compat.
Now hosted by: @suaveplan/i18n-shared
Requirement: Locale descriptors exposed by @suaveplan/i18n
REMOVED. LOCALE_DESCRIPTORS, DEFAULT_LOCALES, DefaultLocale move to @suaveplan/i18n-shared.
Now hosted by: @suaveplan/i18n-shared
Requirement: I18N error code registry hosted by @suaveplan/i18n
REMOVED. I18N_CODES, I18nError, I18nErrorCode (formerly I18nCode) move to @suaveplan/i18n-shared. Re-exported via root barrel for back-compat. The legacy I18nCode name is preserved as a type alias for I18nErrorCode.
Now hosted by: @suaveplan/i18n-shared
Requirement: ICU MessageFormat parser hosted by @suaveplan/i18n
REMOVED. MessageParser, extractVariables, renderNodes, every MessageNode AST type, every ParserOptions type move to @suaveplan/i18n-icu-parser. Re-exported via root barrel.
Now hosted by: @suaveplan/i18n-icu-parser (capability spec: openspec/specs/i18n-icu-parser/spec.md)
Requirement: {{var}} interpolation engine hosted by @suaveplan/i18n
REMOVED. InterpolationEngine, escapeHtmlChars, escapeRegex, every Interpolation* type move to @suaveplan/i18n-icu-parser/interpolation. Re-exported via root barrel.
Now hosted by: @suaveplan/i18n-icu-parser
Requirement: Pluralization with 9 CLDR rule sets hosted by @suaveplan/i18n
REMOVED. PluralizationEngine, every plural-rule function (arabicPlural, chinesePlural, defaultPluralRule, englishPlural, frenchPlural, germanPlural, polishPlural, russianPlural, spanishPlural, welshPlural), getBaseLocale, normalizeLocale, every Plural* type move to @suaveplan/i18n-icu-parser/pluralization. Re-exported via root barrel.
Now hosted by: @suaveplan/i18n-icu-parser
Requirement: Number / RelativeTime / List / DisplayName / Date / Currency formatters hosted by @suaveplan/i18n
REMOVED. Every formatter and helper moves to its dedicated @suaveplan/i18n-formatter-<name> package. The current root barrel exposes formatNumber, formatRelativeTime, formatList, formatDisplayName, formatDate, formatTime, formatDateOnly, formatDateRange, formatCurrency, formatCurrencyValue, getCurrencyInfo, getCurrencyName, getCurrencySymbol, getDayNames, getMonthNames. Every one of these is re-exported via the back-compat root barrel.
Two new formatters (Collator, Segmenter) are ADDED at the family level by Wave 3 and are also re-exported via the slim core's root barrel.
Now hosted by: @suaveplan/i18n-formatter-number, @suaveplan/i18n-formatter-relative-time, @suaveplan/i18n-formatter-list, @suaveplan/i18n-formatter-display-name, @suaveplan/i18n-formatter-collator, @suaveplan/i18n-formatter-segmenter, @suaveplan/i18n-formatter-date, @suaveplan/i18n-formatter-currency
Requirement: Locale detection chain hosted by @suaveplan/i18n
REMOVED. createDetectorChain, DETECTOR_CHAIN_UNRESOLVED, every Detector* type, every individual detector class move to @suaveplan/i18n-detector. Re-exported via root barrel and via the ./detector subpath.
Now hosted by: @suaveplan/i18n-detector
Requirement: Translation backends hosted by @suaveplan/i18n
REMOVED. MemoryBackend, HttpBackend, StorageBackend, MultiFileFsBackend, ChainedBackend, every backend factory function, every backend options type move to @suaveplan/i18n-backend-<name> packages. Universal-tier backends are re-exported via the slim core's root barrel; the server-tier MultiFileFsBackend is NOT re-exported and consumers must import directly from @suaveplan/i18n-backend-fs-multifile.
Now hosted by: @suaveplan/i18n-backend-memory, @suaveplan/i18n-backend-http, @suaveplan/i18n-backend-storage, @suaveplan/i18n-backend-fs-multifile, @suaveplan/i18n-backend-chained
MODIFIED Requirements
Requirement: ICU MessageFormat render integrates formatters via FormatBridge
MODIFIED. Pre-split, MessageCatalog and I18nInstance invoked formatters by direct function import inside parser/message/message-render.ts. Post-split, the parser package declares a FormatBridge interface and accepts a bridge as a render option. The slim @suaveplan/i18n core SHALL construct a defaultFormatBridge by importing the formatter packages and wiring them to the bridge methods, then SHALL pass that bridge to every internal renderNodes call. Consumers calling createI18n(...) SHALL get the default bridge automatically; consumers MAY pass { formatBridge: customBridge } to override.
Scenario: createI18n auto-wires the default FormatBridge
- WHEN
const i18n = createI18n({ catalog: { ... } })is called with noformatBridgeoption - AND a translated key
t("price", { value: 42, currency: "USD" })resolves to"{value, number, currency=USD}" - THEN the rendered string equals
"$42.00"(or platform-locale-equivalent)
Scenario: custom FormatBridge overrides the default
- WHEN
const i18n = createI18n({ catalog, formatBridge: { number: () => "OVERRIDE" } })is called - AND a translated key resolving to
{value, number}is rendered - THEN the rendered string equals
"OVERRIDE"
Scenario: omitting a method on a custom bridge falls back to String coercion
- WHEN
const i18n = createI18n({ catalog, formatBridge: { number: () => "X" } })is called (nocurrencymethod) - AND a translated key resolving to
{amount, number, currency=USD}is rendered - THEN the rendered string equals
"42"(string coercion fallback peri18n-icu-parserspec)
Verified by: packages/i18n/i18n/src/conformance.test.ts::"createI18n auto-wires defaultFormatBridge" AND packages/i18n/i18n/src/conformance.test.ts::"custom formatBridge overrides default" AND packages/i18n/i18n/src/conformance.test.ts::"missing bridge method falls back to String"
Requirement: Public surface preserved via back-compat root barrel
MODIFIED. Pre-split, packages/i18n/i18n/src/index.ts exported 33 statements totalling roughly 80 distinct symbols, all sourced from local files. Post-split, every one of those 80 symbols SHALL continue to resolve through import { X } from "@suaveplan/i18n", but the source of every constant / formatter / parser / backend / detector symbol changes to the relevant Wave 1–5 package. The back-compat re-export map in proposal.md enumerates every symbol's pre/post source.
Scenario: every documented pre-split export resolves post-slim
- WHEN every symbol enumerated in the back-compat re-export map is imported via
from "@suaveplan/i18n" - THEN every import resolves with
typeof <symbol> !== "undefined"
Scenario: subpath exports continue to resolve
- WHEN every documented subpath (
@suaveplan/i18n/detector,@suaveplan/i18n/plugin,@suaveplan/i18n/parser,@suaveplan/i18n/errors) is imported in a consumer fixture - THEN every import resolves with no
Module not founderrors
Scenario: legacy I18nCode name is preserved as type alias
- WHEN a downstream consumer writes
import type { I18nCode } from "@suaveplan/i18n"; - THEN the import resolves and the type equals
I18nErrorCode
Verified by: packages/i18n/i18n/src/index.test.ts::"every back-compat re-export resolves" AND packages/i18n/i18n/src/index.test.ts::"every documented subpath export resolves" AND packages/i18n/i18n/src/index.test.ts::"legacy I18nCode equals I18nErrorCode"
Requirement: Backend factories exposed at root barrel
MODIFIED. Pre-split, backend classes (MemoryBackend, HttpBackend, StorageBackend, MultiFileFsBackend, ChainedBackend) and their factories (memoryBackend, httpBackend, storageBackend, chainedBackend) were exported only via ./core/backend/* subpaths. Post-split, the four universal-tier backends (memory, http, storage, chained) SHALL be exposed at the root barrel for parity with formatter and detector exports. The server-tier MultiFileFsBackend SHALL NOT be exposed at the root barrel and consumers must import directly from @suaveplan/i18n-backend-fs-multifile.
Scenario: universal backends importable from root
- WHEN
import { MemoryBackend, memoryBackend, HttpBackend, httpBackend, StorageBackend, storageBackend, ChainedBackend, chainedBackend } from "@suaveplan/i18n";is evaluated - THEN every symbol resolves
Scenario: server-tier backend NOT importable from root
- WHEN
import { MultiFileFsBackend } from "@suaveplan/i18n";is evaluated - THEN TypeScript reports
Module '"@suaveplan/i18n"' has no exported member 'MultiFileFsBackend'
Verified by: packages/i18n/i18n/src/index.test.ts::"4 universal-tier backend factories importable from root" AND packages/i18n/i18n/src/index.test.ts::"MultiFileFsBackend NOT exported from root"
ADDED Requirements
Requirement: Default FormatBridge factory wires formatter packages
The package SHALL author src/parser/format-bridge-default.ts exporting defaultFormatBridge: FormatBridge. This bridge SHALL import formatNumber, formatCurrency, formatDate, formatTime, formatRelativeTime from their respective Wave 3 packages and bind them to the corresponding FormatBridge methods. The bridge SHALL be passed automatically by createI18n when no override is supplied.
Scenario: defaultFormatBridge implements every FormatBridge method
- WHEN
expectTypeOf(defaultFormatBridge).toEqualTypeOf<Required<FormatBridge>>()runs - THEN the type-level check passes (every optional method is implemented)
Scenario: defaultFormatBridge.number calls into i18n-formatter-number
- WHEN
defaultFormatBridge.number(1234.5, "en-GB", "decimal")is called - THEN the result equals
formatNumber(1234.5, "en-GB", { style: "decimal" })
Verified by: packages/i18n/i18n/src/parser/format-bridge-default.test.ts::"defaultFormatBridge implements every FormatBridge method" AND packages/i18n/i18n/src/parser/format-bridge-default.test.ts::"defaultFormatBridge.number forwards to i18n-formatter-number"
Requirement: Slim-core source inventory enforced
The package SHALL contain ONLY the following directories and files under src/:
src/core/catalog/—MessageCatalog,deepMerge, types, schemas, tests, docssrc/core/plugin/—createI18n,I18nInstance, types, tests, docssrc/locale/negotiation/—negotiateLocale,parseAcceptLanguage, types, tests, docssrc/locale/resolver/—LocaleResolver,extractLanguage,extractRegion,normalizeLocaleCode, types, tests, docssrc/middleware/server/—createLocaleMiddleware,detectLocale,getDirection, types, tests, docssrc/observability/— detector-metrics state and OTel adapterssrc/schemas/—CatalogOptionsSchema,InterpolationConfigSchema,ResolverConfigSchema,TranslationRecordSchema, schema input typessrc/utils/— key utilities (buildKey,countKeys,flattenRecord,getNestedValue,getTextDirection,hasKey,isRtl,parseKey,sanitize,unflattenRecord)src/parser/format-bridge-default.ts— the wiring file added by this changesrc/conformance.test.ts— end-to-end conformance gatesrc/index.ts— back-compat root barrelsrc/index.test.ts— exhaustive symbol-existence test
The package SHALL NOT contain src/constants/, src/error-codes.ts, src/errors/, src/parser/message/, src/core/interpolation/, src/core/pluralization/, src/formatting/, src/detector/, src/core/backend/. A test SHALL enforce this inventory.
Scenario: forbidden directories absent
- WHEN the source tree under
packages/i18n/i18n/src/is enumerated - THEN none of
constants,error-codes.ts,errors,formatting,detectorexist; the onlyparser/entry isformat-bridge-default.tsand its sibling.md/.test.ts
Verified by: packages/i18n/i18n/src/source-inventory.test.ts::"forbidden directories absent post-slim" AND packages/i18n/i18n/src/source-inventory.test.ts::"only allowed directories present"
Requirement: Workspace deps include every Wave 1–5 universal package
The package's package.json dependencies SHALL include every Wave 1–5 package as workspace:^ except the server-tier @suaveplan/i18n-backend-fs-multifile. The fs-multifile backend SHALL be either omitted entirely or declared as optionalPeerDependencies.
Scenario: 15 workspace deps present
- WHEN
packages/i18n/i18n/package.jsonis parsed - THEN
dependenciesincludes the 15 packages enumerated in proposal.md §"Added workspace deps"
Scenario: fs-multifile NOT in dependencies
- WHEN
packages/i18n/i18n/package.jsondependenciesis checked for@suaveplan/i18n-backend-fs-multifile - THEN the key is absent from
dependencies(may appear underoptionalPeerDependenciesor be entirely omitted)
Verified by: packages/i18n/i18n/src/source-inventory.test.ts::"15 universal Wave 1-5 packages declared as workspace deps" AND packages/i18n/i18n/src/source-inventory.test.ts::"fs-multifile NOT in universal-tier dependencies"
Requirement: Bundle size reduction documented
The package SHALL document its post-slim bundle size in README.md and SHALL show a meaningful reduction (target: ≥ 60% smaller than the pre-split monolith bundle). The pre-split and post-slim sizes SHALL be captured in tasks.md §4.4 and §5.16 and reported in the change folder's archive entry.
Scenario: README documents bundle size
- WHEN
README.mdis read - THEN the file contains a "Bundle size" section with a number in KB and a date
Verified by: Manual inspection of packages/i18n/i18n/README.md during pre-archive review (per AGENTS.md §13)