first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
# @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.
<!-- begin:auto-docs -->
## Documentation tree
- [chain](./src/chain/chain.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [bcp47-negotiate](./src/chain/bcp47-negotiate.md) — `bcp47Negotiate(raw, supported, fallback)` is the default negotiator wired into `createDetectorChain` when no `options.…
- [chain](./src/chain/chain.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [bcp47-negotiate](./src/chain/bcp47-negotiate.md) — `bcp47Negotiate(raw, supported, fallback)` is the default negotiator wired into `createDetectorChain` when no `options.…
- [chain](./src/chain/chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [bcp47-negotiate](./src/chain/bcp47-negotiate.md) — `bcp47Negotiate(raw, supported, fallback)` is the default negotiator wired into `createDetectorChain` when no `options.…
- [chain](./src/chain/chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [bcp47-negotiate](./src/chain/bcp47-negotiate.md) — `bcp47Negotiate(raw, supported, fallback)` is the default negotiator wired into `createDetectorChain` when no `options.…
- [chain](./src/chain/chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [bcp47-negotiate](./src/chain/bcp47-negotiate.md) — `bcp47Negotiate(raw, supported, fallback)` is the default negotiator wired into `createDetectorChain` when no `options.…
- [chain](./src/chain/chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_ - [default-chain](./src/default-chain/default-chain.md) — <!-- begin:auto-docs --> _(no submodules)_- [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_ - [accept-language](./src/detectors/accept-language/accept-language.md) — <!-- begin:auto-docs --> _(no submodules)_- [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cloudflare-ip-geo](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) — <!-- begin:auto-docs --> _(no submodules)_- [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cookie](./src/detectors/cookie/cookie.md) — <!-- begin:auto-docs --> _(no submodules)_- [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_ - [fallback](./src/detectors/fallback/fallback.md) — <!-- begin:auto-docs --> _(no submodules)_- [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_ - [html-lang](./src/detectors/html-lang/html-lang.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [local-storage](./src/detectors/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_ - [navigator](./src/detectors/navigator/navigator.md) — <!-- begin:auto-docs --> _(no submodules)_- [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_ - [querystring](./src/detectors/querystring/querystring.md) — <!-- begin:auto-docs --> _(no submodules)_- [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_ - [user-preference](./src/detectors/user-preference/user-preference.md) — <!-- begin:auto-docs --> _(no submodules)_- [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_ - [direction](./src/direction/direction.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(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`)
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
```ts
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](./src/chain/chain.md) |
| `chain/bcp47-negotiate.ts` — default negotiator | [bcp47-negotiate.md](./src/chain/bcp47-negotiate.md) |
| `direction/direction.ts` — direction helpers | [direction.md](./src/direction/direction.md) |
| `detectors/querystring/querystring.ts` | [querystring.md](./src/detectors/querystring/querystring.md) |
| `detectors/user-preference/user-preference.ts` | [user-preference.md](./src/detectors/user-preference/user-preference.md) |
| `detectors/cookie/cookie.ts` | [cookie.md](./src/detectors/cookie/cookie.md) |
| `detectors/local-storage/local-storage.ts` | [local-storage.md](./src/detectors/local-storage/local-storage.md) |
| `detectors/html-lang/html-lang.ts` | [html-lang.md](./src/detectors/html-lang/html-lang.md) |
| `detectors/accept-language/accept-language.ts` | [accept-language.md](./src/detectors/accept-language/accept-language.md) |
| `detectors/navigator/navigator.ts` | [navigator.md](./src/detectors/navigator/navigator.md) |
| `detectors/cloudflare-ip-geo/cloudflare-ip-geo.ts` | [cloudflare-ip-geo.md](./src/detectors/cloudflare-ip-geo/cloudflare-ip-geo.md) |
| `detectors/fallback/fallback.ts` | [fallback.md](./src/detectors/fallback/fallback.md) |
| `default-chain/default-chain.ts` | [default-chain.md](./src/default-chain/default-chain.md) |
| `observability/metrics.ts` | [metrics.md](./src/observability/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