first commit
This commit is contained in:
154
docs/localization/i18n/i18n-detector/README.md
Normal file
154
docs/localization/i18n/i18n-detector/README.md
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user