13 KiB
13 KiB
Tasks — @suaveplan/i18n-detector (locale detection chain)
Every checkbox MUST reflect work actually done in this change. Do not retroactively tick.
1. Spec authoring
- 1.1 Read
openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.mdend-to-end (especially §3 row 11 and §11 out of scope) - 1.2 Read
openspec/AGENTS.mdSections 0, 0a, 6a, 7, 9, 13 - 1.3 Read every source file listed in
## Source mappingtable in proposal.md (chain orchestrator + 8 detectors + default-chain + utils.ts direction helpers) - 1.4 Draft
proposal.mdfromopenspec/templates/proposal.md - 1.5 Fill
## Source mappingtable with line ranges from the actual source files - 1.6 Draft
specs/i18n-detector/spec.mdfromopenspec/templates/spec-delta.md - 1.7 Every requirement carries a
**Verified by:**line naming concreteit(...)titles - 1.8
openspec change validate i18n-detectorexits with no errors
2. Package scaffold
- 2.1
mkdir -p packages/i18n/i18n-detector/src/{chain,direction,detectors/{querystring,user-preference,cookie,local-storage,html-lang,accept-language,navigator,cloudflare-ip-geo,fallback},default-chain} - 2.2 Author
package.json— name@suaveplan/i18n-detector, version0.1.0, typemodule, sideEffectsfalse. Deps:@suaveplan/i18n-shared workspace:^,@suaveplan/locale-data workspace:^,@suaveplan/types workspace:^,@suaveplan/error workspace:^,@opentelemetry/api catalog:observability,tslib catalog:build,zod catalog:forms. devDeps:@suaveplan/testing workspace:^,@suaveplan/typescript-config workspace:^,@vitest/coverage-istanbul catalog:testing,vitest catalog:testing,@types/node catalog:,vite catalog:build,vite-plugin-dts catalog:build,typescript catalog:. - 2.3 Author
tsconfig.jsonextending@suaveplan/typescript-config/library.json - 2.4 Author
vite.config.tsusingcreateTestConfig()from@suaveplan/testing/config - 2.5 Author
README.md(≥200 words, includes Modules table linking to each.md, dual-tier strategy callout, default-chain example) - 2.6 Add
package.jsonexportsmap per proposal.md "Subpath exports" section (root +./chain+./direction+ 9./detectors/<name>+./default-chain)
3. Implementation — types and chain orchestrator
- 3.1 Author
src/detector.types.ts— portDetectorStorageLike,DetectorContext,DetectorEnvironment,LocaleDetector,DetectorChainResultfrom monolith verbatim - 3.2 Author
src/chain/bcp47-negotiate.ts— exportsbcp47Negotiate(raw, supported, fallback): string | nullconsumingLOCALE_BCP47_MAPfrom@suaveplan/i18n-shared/constants/locale-bcp47-map; exact match → language-subtag fallback →null - 3.3 Author
src/chain/chain.ts— portcreateDetectorChain+DETECTOR_CHAIN_UNRESOLVED+DetectorChain+DetectorChainUnresolvedfrom monolith; modify to acceptoptions?: { negotiate?: NegotiateFn; throwOnExhausted?: boolean }; defaultnegotiate=bcp47Negotiate; throwsI18nError(I18N.DETECTOR_CHAIN_EXHAUSTED)whenthrowOnExhausted: trueAND chain returns sentinel; emiti18n.detector.chain.exhausted_totalcounter on every sentinel return - 3.4 Author
src/chain/chain.types.ts—NegotiateFn,CreateDetectorChainOptions - 3.5 Author
src/chain/index.ts— explicit named re-exports
4. Implementation — direction helper
- 4.1 Author
src/direction/direction.ts— portgetTextDirection+isRtlfrompackages/i18n/i18n/src/utils/utils.tsL138-L153; inlinetype TextDirection = "ltr" | "rtl"(drop the cross-folder import) - 4.2 Author
src/direction/index.ts— explicit named re-exports
5. Implementation — individual detectors
- 5.1 Author
src/detectors/querystring/{querystring.ts,querystring.types.ts,index.ts}— port frompackages/react/react-i18n/src/detector/querystring/; rewireLocaleDetectorimport to../../detector.types.js - 5.2 Author
src/detectors/user-preference/{user-preference.ts,user-preference.types.ts,index.ts}— port from react-i18n; replaceConsoleLoggerdep withoptions.logger?: LoggerLike(default no-op); threadctx.signalon every fetch - 5.3 Author
src/detectors/cookie/{cookie.ts,cookie.types.ts,index.ts}— verbatim port from react-i18n - 5.4 Author
src/detectors/local-storage/{local-storage.ts,local-storage.types.ts,index.ts}— verbatim port from react-i18n - 5.5 Author
src/detectors/html-lang/{html-lang.ts,index.ts}— verbatim port from react-i18n - 5.6 Author
src/detectors/accept-language/{accept-language.ts,accept-language.types.ts,index.ts}— verbatim port from react-i18n - 5.7 Author
src/detectors/navigator/{navigator.ts,index.ts}— verbatim port from react-i18n - 5.8 Author
src/detectors/cloudflare-ip-geo/{cloudflare-ip-geo.ts,cloudflare-ip-geo.types.ts,index.ts}— verbatim port from react-i18n;getAllCountriesimport already targets@suaveplan/locale-data - 5.9 Author
src/detectors/fallback/{fallback.ts,index.ts}— port; replacethrow new Error(...)withthrow new I18nError(I18N_CODES.INVALID_LOCALE, ...)
6. Implementation — default chain factory
- 6.1 Author
src/default-chain/{default-chain.ts,default-chain.types.ts,index.ts}— portdefaultDetectorChain+DefaultDetectorChainOptionsfrom react-i18n; rewire detector imports to sibling folders
7. Per-detector OTel instrumentation
- 7.1 Add
i18n.detector.<name>.duration_mshistogram to every async detector'sdetectAsyncbody (start/stop wall-time) - 7.2 Add
i18n.detector.<name>.detected_total{locale=<resolved>}counter increment to every detector's success path (syncdetect+ asyncdetectAsync) - 7.3 Add
i18n.detector.chain.exhausted_totalcounter to chain orchestrator's sentinel-return branch - 7.4 Verify
getDetectorMetrics()helper from observability lives in this package (port the metrics module from monolithsrc/observability/detector-metrics.ts); rename tosrc/observability/metrics.tsco-located with the chain
8. Root barrel
- 8.1 Author
src/index.ts— explicit named re-exports acrosschain/,direction/,detectors/,default-chain/, anddetector.types.ts(NOexport *per CLAUDE.md §10)
9. Documentation (sibling .md per implementation file)
- 9.1
src/chain/chain.md(≥200 words, basic + advanced examples; covers sync vs async chain, environment gating, AbortSignal, throw-on-exhausted) - 9.2
src/chain/bcp47-negotiate.md(basic + advanced; covers default vs injected negotiator) - 9.3
src/direction/direction.md(basic + advanced; coversgetTextDirection+isRtl) - 9.4
src/detectors/querystring/querystring.md - 9.5
src/detectors/user-preference/user-preference.md - 9.6
src/detectors/cookie/cookie.md - 9.7
src/detectors/local-storage/local-storage.md - 9.8
src/detectors/html-lang/html-lang.md - 9.9
src/detectors/accept-language/accept-language.md - 9.10
src/detectors/navigator/navigator.md - 9.11
src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md - 9.12
src/detectors/fallback/fallback.md - 9.13
src/default-chain/default-chain.md(lists the canonical 8/9-entry order; explains userPreference opt-in) - 9.14
src/observability/metrics.md - 9.15
README.md## Modulessection links every.md
10. Tests (Vitest, runner-neutral imports per CLAUDE.md §11)
- 10.1
src/chain/chain.test.ts— sync chain registration order; async chain race-to-first; environment gating skips browser detectors on server (and vice versa);cache()walks every persisting detector; AbortSignal threaded into every async call;throwOnExhausted: trueraisesI18nError(I18N.DETECTOR_CHAIN_EXHAUSTED); defaultbcp47Negotiatewired - 10.2
src/chain/bcp47-negotiate.test.ts— exact-match path; language-subtag fallback (zh-Hant-TW→zh); short-code expansion viaLOCALE_BCP47_MAP(en→en-GB);nullon miss - 10.3
src/direction/direction.test.ts—getTextDirection("ar")is"rtl";getTextDirection("ar-SA")is"rtl"(subtag extraction);getTextDirection("en-GB")is"ltr";isRtlmirrors - 10.4
src/detectors/querystring/querystring.test.ts— server?lang=frresolves; browserglobalThis.location.searchresolves; missing param returnsnull; both contexts absent returnsnull; customparamNamehonoured - 10.5
src/detectors/user-preference/user-preference.test.ts— happy GET round-trip; non-2xx logged +null; rejected fetch logged +null; AbortSignal threaded; pre-aborted signal returnsnull; cache POST round-trip; cache failure non-fatal;extractAuthinvoked - 10.6
src/detectors/cookie/cookie.test.ts— server reads inbound Cookie header; server writes Set-Cookie on responseHeaders; browser reads/writesdocument.cookie; URL-encoding round-trip; SameSite=lax/strict/noneformatting; missing both contexts returnsnull - 10.7
src/detectors/local-storage/local-storage.test.ts— explicit storage option;globalThis.localStoragefallback; missing storage returnsnull; quota / private-mode error caught; cache failure non-fatal - 10.8
src/detectors/html-lang/html-lang.test.ts—ctx.documentlookup;globalThis.documentfallback; missing root returnsnull; emptylangreturnsnull; whitespace-onlylangreturnsnull - 10.9
src/detectors/accept-language/accept-language.test.ts— q-value sort desc; stable order on ties;*wildcard skipped; malformed q skipped; q out of range skipped; missing header returnsnull; empty header returnsnull - 10.10
src/detectors/navigator/navigator.test.ts—navigator.languagespreferred overnavigator.language; whitespace-only entries skipped; missing navigator returnsnull - 10.11
src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.test.ts—AE→ar-AEvia locale-data; override map wins over locale-data; missing header returnsnull; unknown country returnsnull; lowercase header value upper-cased - 10.12
src/detectors/fallback/fallback.test.ts— returns configured locale; empty locale throwsI18nError(I18N.INVALID_LOCALE); whitespace-only locale throws - 10.13
src/default-chain/default-chain.test.ts— order matches spec exactly;userPreferenceonly when endpoint set; fresh array per call; fallback always last; environment values match - 10.14
src/index.test.ts— every documented public export resolves through the root barrel; every documented subpath export resolves - 10.15 Every
it(...)carries{ timeout: 5000 } - 10.16 All test imports go through
@suaveplan/testing/runner(CLAUDE.md §11) — verified bygrep -rnE "from ['\"](vitest|bun:test)['\"]" packages/i18n/i18n-detector/src/returning zero hits
11. Pre-archive verification (5-gate triple)
- 11.1
bun run check— Biome 0/0/0 across the monorepo - 11.2
bunx turbo lint typecheck test --filter=@suaveplan/i18n-detector— green - 11.3
bunx turbo test:coverage --filter=@suaveplan/i18n-detector— 100/100/100/100 - 11.4
bun scripts/openspec-conformance.ts @suaveplan/i18n-detector— exit 0, noerrorfindings - 11.5
bunx turbo build --filter=@suaveplan/i18n-detector— green
12. Pre-archive 7-row checklist (AGENTS.md Section 13)
- 12.1 Generics threaded —
LocaleDetectorinterface preserves the upstream shape;NegotiateFntyped as(raw: string, supported: readonly string[], fallback: string) => string | null;CreateDetectorChainOptionsuses precise types notany - 12.2 Zod schemas wired at every public boundary —
DetectorContextSchema,DefaultDetectorChainOptionsSchema,QuerystringDetectorOptionsSchema,CookieDetectorOptionsSchema,LocalStorageDetectorOptionsSchema,UserPreferenceDetectorOptionsSchema,CloudflareIpGeoDetectorOptionsSchema,CreateDetectorChainOptionsSchema - 12.3 Every error code thrown by at least one production path:
I18N.INVALID_LOCALE(fallback detector),I18N.DETECTOR_CHAIN_EXHAUSTED(chain whenthrowOnExhausted: true) - 12.4 Strict-mode tested — every browser-only detector's "missing global" branch covered; every server-only detector's "missing request" branch covered; AbortSignal-aborted detectAsync returns
nulldeterministically - 12.5 OTel — every async detector emits
i18n.detector.<name>.duration_ms; every detector emitsi18n.detector.<name>.detected_total{locale=<resolved>}on success; chain emitsi18n.detector.chain.exhausted_totalon sentinel return;i18n.detectspan setsresolved_localeandwinning_detectorattributes - 12.6 Lifecycle dedup — N/A (detectors are pure factories returning fresh
LocaleDetectorper call; chain is stateless) - 12.7 No retroactive tick — every box reflects work done in this change
13. Manifest + ecosystem awareness
- 13.1 Add
@suaveplan/i18n-detectorentry to rootCLAUDE.mdpackage listing under the i18n cluster (one line, mentions universal tier + 8 detectors + chain + direction helper) - 13.2 Run
bun scripts/folder-reorg/generate-manifest.tsto refreshMANIFEST.md - 13.3 Append
@suaveplan/i18n-detectortoSTATUS.mdCompleted list
14. Archive
- 14.1 Fold spec delta into
openspec/specs/i18n-detector/spec.md(NEW capability spec — first delta becomes canonical) - 14.2 Move
openspec/changes/i18n-detector/→openspec/archive/<yyyy-mm-dd>-i18n-detector/