20 KiB
@suaveplan/i18n-detector - Universal locale-detection chain for the @suaveplan/i18n family. Ships the chain orchestrator, the BCP-47 default negotiator, the getTextDirection / isRtl helpers, and 9 detector implementations (querystring, user-preference, cookie, localStorage, html-lang, Accept-Language, navigator, Cloudflare IP-geo, fallback). Browser-only and server-only detectors gate on runtime probes so the same package runs in either tier without conditional package.json exports.
Summary
Universal locale-detection chain for the @suaveplan/i18n
family. Ships the chain orchestrator, the BCP-47 default
negotiator, the getTextDirection / isRtl direction helpers, and
nine detector implementations (querystring, userPreference,
cookie, localStorage, htmlLang, acceptLanguage,
navigator, cloudflareIpGeo, fallback) plus a canonical
defaultDetectorChain factory.
Documentation tree
- chain — (no submodules)
- bcp47-negotiate —
bcp47Negotiate(raw, supported, fallback)is the default negotiator wired intocreateDetectorChainwhen no `options.… - chain — (no submodules)
- bcp47-negotiate —
- default-chain — (no submodules)
- default-chain — (no submodules)
- accept-language — (no submodules)
- accept-language — (no submodules)
- cloudflare-ip-geo — (no submodules)
- cloudflare-ip-geo — (no submodules)
- cookie — (no submodules)
- cookie — (no submodules)
- fallback — (no submodules)
- fallback — (no submodules)
- html-lang — (no submodules)
- html-lang — (no submodules)
- local-storage — (no submodules)
- local-storage — (no submodules)
- navigator — (no submodules)
- navigator — (no submodules)
- querystring — (no submodules)
- querystring — (no submodules)
- user-preference — (no submodules)
- user-preference — (no submodules)
- direction — (no submodules)
- direction — (no submodules)
- observability — (no submodules)
- metrics — (no submodules)
- chain — (no submodules)- default-chain — (no submodules)- default-chain — (no submodules)- accept-language — (no submodules)- accept-language — (no submodules)- cloudflare-ip-geo — (no submodules)- cloudflare-ip-geo — (no submodules)- cookie — (no submodules)- cookie — (no submodules)- fallback — (no submodules)- fallback — (no submodules)- html-lang — (no submodules)- html-lang — (no submodules)- local-storage — (no submodules)- local-storage — (no submodules)- navigator — (no submodules)- navigator — (no submodules)- querystring — (no submodules)- querystring — (no submodules)- user-preference — (no submodules)- user-preference — (no submodules)- direction — (no submodules)- direction — (no submodules)- observability — (no submodules)- metrics — (no submodules)- bcp47-negotiate —
bcp47Negotiate(raw, supported, fallback)is the default negotiator wired intocreateDetectorChainwhen no `options.… - chain — (no submodules)- default-chain — (no submodules)- default-chain — (no submodules)- accept-language — (no submodules)- accept-language — (no submodules)- cloudflare-ip-geo — (no submodules)- cloudflare-ip-geo — (no submodules)- cookie — (no submodules)- cookie — (no submodules)- fallback — (no submodules)- fallback — (no submodules)- html-lang — (no submodules)- html-lang — (no submodules)- local-storage — (no submodules)- local-storage — (no submodules)- navigator — (no submodules)- navigator — (no submodules)- querystring — (no submodules)- querystring — (no submodules)- user-preference — (no submodules)- user-preference — (no submodules)- direction — (no submodules)- direction — (no submodules)- observability — (no submodules)- metrics — (no submodules)- bcp47-negotiate —
bcp47Negotiate(raw, supported, fallback)is the default negotiator wired intocreateDetectorChainwhen no `options.… - chain — (no submodules)- default-chain — (no submodules)- default-chain — (no submodules)- accept-language — (no submodules)- accept-language — (no submodules)- cloudflare-ip-geo — (no submodules)- cloudflare-ip-geo — (no submodules)- cookie — (no submodules)- cookie — (no submodules)- fallback — (no submodules)- fallback — (no submodules)- html-lang — (no submodules)- html-lang — (no submodules)- local-storage — (no submodules)- local-storage — (no submodules)- navigator — (no submodules)- navigator — (no submodules)- querystring — (no submodules)- querystring — (no submodules)- user-preference — (no submodules)- user-preference — (no submodules)- direction — (no submodules)- direction — (no submodules)- observability — (no submodules)- metrics — (no submodules)- bcp47-negotiate —
bcp47Negotiate(raw, supported, fallback)is the default negotiator wired intocreateDetectorChainwhen no `options.… - chain — (no submodules)- default-chain — (no submodules) - default-chain — (no submodules)- accept-language — (no submodules) - accept-language — (no submodules)- cloudflare-ip-geo — (no submodules) - cloudflare-ip-geo — (no submodules)- cookie — (no submodules) - cookie — (no submodules)- fallback — (no submodules) - fallback — (no submodules)- html-lang — (no submodules) - html-lang — (no submodules)- local-storage — (no submodules) - local-storage — (no submodules)- navigator — (no submodules) - navigator — (no submodules)- querystring — (no submodules) - querystring — (no submodules)- user-preference — (no submodules) - user-preference — (no submodules)- direction — (no submodules) - direction — (no submodules)- observability — (no submodules) - metrics — (no submodules)
Why this package exists
Locale detection is the single most architecturally mis-placed
piece of the i18n stack. The chain orchestrator lived in the
universal @suaveplan/i18n monolith, but every detector
implementation lived in @suaveplan/react-i18n — a browser-tier
React binding package — even though most detectors are framework-
agnostic and have no DOM dependency at all. Server-only detectors
(acceptLanguage, cloudflareIpGeo) were unreachable from a
Node-only consumer that didn't want to install React.
This package consolidates every detector implementation, the
chain orchestrator, the standalone getTextDirection helper, and
the defaultDetectorChain factory into a single universal
package that depends only on @suaveplan/i18n-shared,
@suaveplan/locale-data, and @suaveplan/types.
Dual-tier strategy
The detector chain runs in both browser and server contexts. Universal means every detector MUST gracefully short-circuit when the runtime it expects is absent:
- Browser-only (
htmlLang,localStorage,navigator) probeglobalThis.document/globalThis.localStorage/globalThis.navigatorand returnnullwhen the global is absent. The chain additionally honoursenvironment: "browser"so server runs skip them entirely. - Server-only (
acceptLanguage,cloudflareIpGeo) gate onctx.request !== undefined. Theirenvironment: "server"causes the chain to skip them on a browser run. - Universal (
querystring,cookie,userPreference,fallback) carryenvironment: "both"and fall back throughctx.request → globalThislookups so they work in either tier.
No conditional package.json exports; no node: import in any
source file.
Quick start
import {
createDetectorChain,
defaultDetectorChain,
} from "@suaveplan/i18n-detector";
const chain = createDetectorChain(
defaultDetectorChain({ fallback: "en-GB" })
);
const result = await chain.detect({
request: {
headers: new Headers({ "accept-language": "fr-FR,en;q=0.5" }),
url: new URL("https://example.com/?lang=fr-FR"),
},
supportedLocales: ["en-GB", "fr-FR"],
fallbackLocale: "en-GB",
});
Modules
| Module | Doc |
|---|---|
chain/chain.ts — chain orchestrator |
chain.md |
chain/bcp47-negotiate.ts — default negotiator |
bcp47-negotiate.md |
direction/direction.ts — direction helpers |
direction.md |
detectors/querystring/querystring.ts |
querystring.md |
detectors/user-preference/user-preference.ts |
user-preference.md |
detectors/cookie/cookie.ts |
cookie.md |
detectors/local-storage/local-storage.ts |
local-storage.md |
detectors/html-lang/html-lang.ts |
html-lang.md |
detectors/accept-language/accept-language.ts |
accept-language.md |
detectors/navigator/navigator.ts |
navigator.md |
detectors/cloudflare-ip-geo/cloudflare-ip-geo.ts |
cloudflare-ip-geo.md |
detectors/fallback/fallback.ts |
fallback.md |
default-chain/default-chain.ts |
default-chain.md |
observability/metrics.ts |
metrics.md |
Subpath exports
Every detector ships under its own subpath so consumers can import only the surfaces they need (tree-shake-friendly):
@suaveplan/i18n-detector— root barrel@suaveplan/i18n-detector/chain@suaveplan/i18n-detector/direction@suaveplan/i18n-detector/default-chain@suaveplan/i18n-detector/detectors/<name>for each of the nine detectors
Observability
i18n.detectspan — wraps every chain run; setsresolved_localeandwinning_detectorattributes.i18n.detector.match_total{detector}— aggregate match counter.i18n.detector.detected_total{detector,locale}— per-locale match counter.i18n.detector.duration_ms{detector}— async detector wall-time histogram.i18n.detector.chain.exhausted_total— chain-exhaustion counter.
License
MIT