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

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

- [bcp47-negotiate](./src/chain/bcp47-negotiate.md) — `bcp47Negotiate(raw, supported, fallback)` is the default negotiator wired into `createDetectorChain` when no `options.…

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) probe globalThis.document / globalThis.localStorage / globalThis.navigator and return null when the global is absent. The chain additionally honours environment: "browser" so server runs skip them entirely.
  • Server-only (acceptLanguage, cloudflareIpGeo) gate on ctx.request !== undefined. Their environment: "server" causes the chain to skip them on a browser run.
  • Universal (querystring, cookie, userPreference, fallback) carry environment: "both" and fall back through ctx.request → globalThis lookups 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.detect span — wraps every chain run; sets resolved_locale and winning_detector attributes.
  • 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