Unified, strategy-based language detection for Node.js, Angular SSR, and the browser.
One package to detect your user's language from URL paths, query strings, cookies, geo-IP, JWT tokens, headers — with fully configurable priority, zero lock-in, and first-class TypeScript support.
- Why This Package?
- Quick Start
- Installation
- Detection Stages
- Package Structure
- API Reference
- Usage Guide: Node.js / Express
- Usage Guide: Angular SSR
- Usage Guide: Angular (Browser-Only)
- Usage Guide: Any TypeScript / Node.js Service
- Configuration Reference
- Advanced Usage
- How Detection Works
- Testing
- License
| Problem | Solution |
|---|---|
| Language detection duplicated across 3+ projects | One package, import everywhere |
| Hard-coded priority order | Configurable stage array — reorder, add, remove |
| Server-only or browser-only | Works in Node.js, Angular SSR, and browser |
| Different query param names across projects | Configurable keys for cookies, query params, geo params |
| No typed API | Full TypeScript with exported interfaces and enums |
| Can't extend detection logic | Strategy pattern — plug in custom stages |
npm install @posty5/lang-detectExpress (one line):
import { langDetectMiddleware } from "@posty5/lang-detect";
app.use(langDetectMiddleware());
// → res.locals['lang'] = 'ar'
// → res.locals['isRTL'] = true
// → res.locals['langDetectedBy'] = 'PATH_LANG'Browser / Angular:
import { detectLanguage, createBrowserContext } from "@posty5/lang-detect";
const result = await detectLanguage(createBrowserContext());
console.log(result.lang); // 'en'# npm
npm install @posty5/lang-detect
# yarn
yarn add @posty5/lang-detect
# pnpm
pnpm add @posty5/lang-detectOptional peer dependency for IP-based geo detection (server-only):
npm install geoip-countryIf you don't install
geoip-country, theVISITOR_GEOstage will be skipped unless you provide a customgeoDetectorfunction.
The detector runs through stages in order. The first stage that returns a result wins.
| # | Stage | What it does | Example | Browser? |
|---|---|---|---|---|
| 1 | PATH_LANG |
Language code in URL path | /en/dashboard → en |
Yes |
| 2 | QUERY_LANG |
Language in query string | ?lang=ar → ar |
Yes |
| 3 | GEO_PATH |
Geo code in URL path → language | /us/trends → en |
Yes |
| 4 | GEO_QUERY |
Geo code in query string → language | ?locale=eg → ar |
Yes |
| 5 | COOKIE |
Language stored in cookie | Cookie lang=fr → fr |
Yes |
| 6 | USER_LANG |
Logged-in user's saved preference | JWT { lang: 'ko' } → ko |
Yes |
| 7 | VISITOR_GEO |
IP → country → language | IP 41.x.x.x → EG → ar |
Yes (with IP) |
| 8 | ACCEPT_LANGUAGE |
Browser/header language preference | Accept-Language: es → es |
Yes (adapted) |
| 9 | DEFAULT |
Fallback | — → en |
Yes |
Tip: Pass your own
stagesarray to change the order or use only the stages you need.
@posty5/lang-detect
├── src/
│ ├── index.ts # Barrel export (everything re-exported)
│ │
│ ├── enums/
│ │ └── detection-stage.enum.ts # DetectionStage enum (9 stages)
│ │
│ ├── interfaces/
│ │ ├── config.interface.ts # ILangDetectConfig, ICookieOptions
│ │ ├── context.interface.ts # IDetectionContext
│ │ ├── result.interface.ts # IDetectionResult
│ │ └── strategy.interface.ts # IDetectionStrategy, IResolvedConfig
│ │
│ ├── data/
│ │ ├── supported-languages.ts # Default 15 languages
│ │ ├── geo-to-lang.ts # Default 45+ country → language map
│ │ └── rtl-languages.ts # RTL language list ['ar', 'ur']
│ │
│ ├── strategies/ # Strategy Pattern — one class per stage
│ │ ├── path-lang.strategy.ts # /en/page → en
│ │ ├── query-lang.strategy.ts # ?lang=ar → ar
│ │ ├── geo-path.strategy.ts # /us/page → en (via GEO_TO_LANG)
│ │ ├── geo-query.strategy.ts # ?locale=eg → ar (via GEO_TO_LANG)
│ │ ├── cookie.strategy.ts # Cookie lang=fr → fr
│ │ ├── user-lang.strategy.ts # context.userLang → user's saved lang
│ │ ├── visitor-geo.strategy.ts # IP → country → language
│ │ ├── accept-language.strategy.ts # Accept-Language header / navigator.languages
│ │ ├── default-lang.strategy.ts # Fallback to config.defaultLanguage
│ │ └── index.ts # Strategy registry + factory
│ │
│ ├── core/
│ │ └── detector.ts # detectLanguage() + resolveConfig()
│ │
│ ├── adapters/
│ │ ├── express.adapter.ts # createExpressContext(req)
│ │ └── browser.adapter.ts # createBrowserContext()
│ │
│ └── middleware/
│ └── express.middleware.ts # langDetectMiddleware() for Express
│
├── tests/
│ ├── detector.test.ts # 52 tests — all stages, priority, RTL, config
│ ├── middleware.test.ts # 7 tests — Express middleware
│ ├── express-adapter.test.ts # 5 tests — context extraction
│ └── test-app.ts # Manual test Express server (port 3456)
│
├── dist/ # Build output
│ ├── index.js # CommonJS (13.7 KB)
│ ├── index.mjs # ESM (13.2 KB)
│ └── index.d.ts # TypeScript declarations
│
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── jest.config.js
The core detection function. Works in any environment (Node.js, browser, SSR).
function detectLanguage(context: IDetectionContext, config?: ILangDetectConfig): Promise<IDetectionResult>;| Param | Type | Description |
|---|---|---|
context |
IDetectionContext |
Environment-agnostic context (from adapter or manual) |
config |
ILangDetectConfig |
Optional. All fields have defaults. |
Returns: Promise<IDetectionResult> — always resolves (never throws).
import { detectLanguage, createExpressContext } from "@posty5/lang-detect";
const context = createExpressContext(req);
const result = await detectLanguage(context);
// result = {
// lang: 'ar',
// detectedBy: 'PATH_LANG',
// isRTL: true
// }Express middleware. Drop it in and it handles everything.
function langDetectMiddleware(
config?: ILangDetectConfig & {
userLangResolver?: (req, res) => string | null | undefined;
},
): RequestHandler;Sets on res.locals:
| Key | Type | Description |
|---|---|---|
lang |
string |
Detected language code ('en', 'ar', etc.) |
isRTL |
boolean |
true for Arabic and Urdu |
langDetectedBy |
string |
Which DetectionStage matched |
Sets cookie: lang=<detected> (365 days, configurable, disable with setCookie: false).
Creates an IDetectionContext from an Express Request object.
function createExpressContext(req: Request, options?: {
userLang?: string | null;
ip?: string | null;
}): IDetectionContext;Extracts path, query, cookies, headers, and ip (from x-forwarded-for, x-real-ip, or req.ip). Pass options.ip to override the auto-detected IP.
Creates an IDetectionContext from the browser environment.
function createBrowserContext(options?: {
userLang?: string | null;
ip?: string | null;
}): IDetectionContext;Reads window.location, document.cookie, and navigator.languages.
By default, ip is null and the VISITOR_GEO stage is skipped. If you pass an IP (e.g. fetched from an external service), the VISITOR_GEO stage will run and detect language based on geo-IP.
All fields are optional. Defaults are applied automatically.
interface ILangDetectConfig {
supportedLanguages?: string[]; // Default: 15 languages
stages?: DetectionStage[]; // Default: all 9 in order
geoToLang?: Record<string, string>; // Default: 45+ country codes
cookieKeys?: string[]; // Default: ['lang']
queryLangKeys?: string[]; // Default: ['lang','language','langCode','languageCode']
geoQueryKeys?: string[]; // Default: ['locale','culture','region','country']
defaultLanguage?: string; // Default: 'en'
geoDetector?: (ip: string) => Promise<string | null> | string | null;
pathSegmentIndex?: number; // Default: 0
setCookie?: boolean; // Default: true
cookieOptions?: ICookieOptions; // Default: { maxAge: 1yr, httpOnly: false, sameSite: 'lax' }
}interface IDetectionContext {
path: string; // URL path, e.g. '/en/page'
queryParams: Record<string, string>; // Query params as key-value
cookies: Record<string, string>; // Cookies as key-value
headers: Record<string, string>; // HTTP headers (lowercase keys)
userLang?: string | null; // Logged-in user's language (from DB, JWT, etc.)
ip?: string | null; // Client IP (null in browser)
navigatorLanguages?: string[]; // Browser's navigator.languages
}interface IDetectionResult {
lang: string; // 'en', 'ar', 'fr', etc.
detectedBy: DetectionStage; // Which stage detected the language
isRTL: boolean; // true for 'ar' and 'ur'
}enum DetectionStage {
PATH_LANG = "PATH_LANG",
QUERY_LANG = "QUERY_LANG",
GEO_PATH = "GEO_PATH",
GEO_QUERY = "GEO_QUERY",
COOKIE = "COOKIE",
USER_LANG = "USER_LANG",
VISITOR_GEO = "VISITOR_GEO",
ACCEPT_LANGUAGE = "ACCEPT_LANGUAGE",
DEFAULT = "DEFAULT",
}import express from "express";
import cookieParser from "cookie-parser";
import { langDetectMiddleware } from "@posty5/lang-detect";
const app = express();
app.use(cookieParser());
app.use(langDetectMiddleware());
app.get("*", (req, res) => {
res.json({
lang: res.locals["lang"], // 'en', 'ar', 'fr', etc.
isRTL: res.locals["isRTL"], // true / false
source: res.locals["langDetectedBy"],
});
});What happens:
| Request | lang |
detectedBy |
|---|---|---|
GET /ar/page |
ar |
PATH_LANG |
GET /page?lang=fr |
fr |
QUERY_LANG |
GET /us/trends |
en |
GEO_PATH |
GET /page?locale=eg |
ar |
GEO_QUERY |
GET /page + Cookie lang=de |
de |
COOKIE |
GET /page + Accept-Language: es |
es |
ACCEPT_LANGUAGE |
GET /page |
en |
DEFAULT |
Override the default stage order. Only the stages you list will run, in that exact order.
app.use(
langDetectMiddleware({
stages: [
DetectionStage.COOKIE, // 1. Check cookie first
DetectionStage.USER_LANG, // 2. Then logged-in user
DetectionStage.QUERY_LANG, // 3. Then query string
DetectionStage.ACCEPT_LANGUAGE, // 4. Then browser header
// DEFAULT is auto-appended
],
}),
);Note: If you omit
DEFAULTfrom your array, it is automatically appended as the last stage.
Use userLangResolver to extract the user's language from your auth middleware:
// Your auth middleware runs first and sets res.locals.user
app.use(authMiddleware);
app.use(
langDetectMiddleware({
userLangResolver: (req, res) => {
// From database user record
return res.locals.user?.languageCode ?? null;
},
}),
);Or from a JWT token:
app.use(
langDetectMiddleware({
userLangResolver: (req) => {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return null;
try {
const decoded = jwt.verify(token, SECRET);
return decoded.lang ?? null;
} catch {
return null;
}
},
}),
);Option A: Using geoip-country (recommended for server-side)
npm install geoip-countryNo extra config needed — the VISITOR_GEO stage auto-detects geoip-country:
app.use(langDetectMiddleware());
// VISITOR_GEO will use geoip-country automatically if installedOption B: Custom API call
app.use(
langDetectMiddleware({
geoDetector: async (ip) => {
const res = await fetch(`https://api.example.com/geo?ip=${ip}`);
const data = await res.json();
return data.countryCode ?? null; // Return 'US', 'EG', etc. or null
},
}),
);Option C: Using CDN headers (Cloudflare, Vercel, etc.)
If your CDN sets country headers, the middleware already reads x-forwarded-for and x-real-ip. For geo detection without IP lookup, you can create a custom geoDetector that reads from headers:
app.use(
langDetectMiddleware({
geoDetector: async (ip) => {
// This runs per-request, but you might prefer
// a dedicated stage. See "Custom Strategy" in Advanced Usage.
return null;
},
}),
);
// OR — just use a prior middleware to set userGeo and map it yourself
app.use((req, res, next) => {
const country = req.headers["cf-ipcountry"] || req.headers["x-country-code"];
if (country) {
res.locals["userGeo"] = country;
}
next();
});app.use(
langDetectMiddleware({
// Check these cookie names in order
cookieKeys: ["user_language", "preferred_lang", "lang"],
// Check these query param names for language
queryLangKeys: ["lng", "lang", "language"],
// Check these query param names for geo code
geoQueryKeys: ["geo", "locale", "country"],
}),
);app.use(
langDetectMiddleware({
setCookie: false,
}),
);Or customize cookie options:
app.use(
langDetectMiddleware({
cookieOptions: {
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days instead of 365
httpOnly: true,
secure: true,
sameSite: "strict",
},
}),
);For Angular SSR projects, the language is detected on the server and transferred to the browser via Angular's TransferState.
In your Angular SSR Express server (server.ts):
import express from "express";
import cookieParser from "cookie-parser";
import { langDetectMiddleware, DetectionStage } from "@posty5/lang-detect";
const app = express();
app.use(cookieParser());
// Detect language before Angular handles the request
app.use(
langDetectMiddleware({
supportedLanguages: ["en", "ar", "fr", "de", "es"],
userLangResolver: (_req, res) => res.locals.loggedUserInfo?.languageCode ?? null,
}),
);When rendering Angular pages, pass the detected language through REQUEST_CONTEXT:
// In your Angular SSR handler / buildAngularPage function:
import { CommonEngine } from "@angular/ssr/node";
const engine = new CommonEngine();
app.get("*", async (req, res) => {
const html = await engine.render({
bootstrap: AppServerModule,
documentFilePath: indexHtml,
url: req.url,
providers: [
{
provide: "REQUEST_CONTEXT",
useValue: {
lang: res.locals["lang"], // ← from langDetectMiddleware
isRTL: res.locals["isRTL"], // ← from langDetectMiddleware
},
},
],
});
res.send(html);
});Create a service that reads the language on both server and client:
// src/app/core/services/language-detection.service.ts
import { Injectable, inject, makeStateKey, TransferState, REQUEST_CONTEXT, PLATFORM_ID } from "@angular/core";
import { isPlatformServer } from "@angular/common";
const LANG_STATE_KEY = makeStateKey<string>("ssrLang");
const RTL_STATE_KEY = makeStateKey<boolean>("ssrIsRTL");
@Injectable({ providedIn: "root" })
export class LanguageDetectionService {
private readonly platformId = inject(PLATFORM_ID);
private readonly transferState = inject(TransferState);
private readonly requestContext = inject(REQUEST_CONTEXT, { optional: true }) as any;
readonly lang: string;
readonly isRTL: boolean;
constructor() {
if (isPlatformServer(this.platformId)) {
// SERVER: Read from REQUEST_CONTEXT (set by middleware)
this.lang = this.requestContext?.["lang"] ?? "en";
this.isRTL = this.requestContext?.["isRTL"] ?? false;
// Store in TransferState for client
this.transferState.set(LANG_STATE_KEY, this.lang);
this.transferState.set(RTL_STATE_KEY, this.isRTL);
} else {
// BROWSER: Read from TransferState (set by server)
this.lang = this.transferState.get(LANG_STATE_KEY, "en");
this.isRTL = this.transferState.get(RTL_STATE_KEY, false);
}
}
}// src/app/pages/home/home.component.ts
import { Component, inject } from "@angular/core";
import { LanguageDetectionService } from "../../core/services/language-detection.service";
@Component({
selector: "app-home",
template: `
<div [dir]="langService.isRTL ? 'rtl' : 'ltr'">
<p>Detected Language: {{ langService.lang }}</p>
</div>
`,
})
export class HomeComponent {
readonly langService = inject(LanguageDetectionService);
}Browser Request
│
▼
Express Server
│
├─ cookieParser()
├─ langDetectMiddleware() ← detects language
│ └─ sets res.locals['lang'], res.locals['isRTL']
│
▼
Angular SSR Engine
│
├─ REQUEST_CONTEXT = { lang, isRTL }
├─ LanguageDetectionService reads REQUEST_CONTEXT
├─ Stores in TransferState
│
▼
HTML Response (includes TransferState data)
│
▼
Browser Hydration
│
├─ LanguageDetectionService reads TransferState
└─ lang + isRTL available instantly (no flash)
For Angular apps without SSR, or when you need to re-detect language on the client side.
import { detectLanguage, createBrowserContext } from "@posty5/lang-detect";
// Reads window.location, document.cookie, navigator.languages
const context = createBrowserContext();
const result = await detectLanguage(context);
console.log(result.lang); // 'en'
console.log(result.detectedBy); // 'COOKIE' or 'ACCEPT_LANGUAGE' etc.
console.log(result.isRTL); // falseTip: To enable
VISITOR_GEOin the browser, pass an IP address viaoptions.ip(see With Custom IP below).
If you want geo-based language detection in the browser, fetch the user's IP from an external service and pass it:
import { detectLanguage, createBrowserContext } from "@posty5/lang-detect";
// Fetch user IP from any IP service
const ip = await fetch("https://api.ipify.org?format=json")
.then((r) => r.json())
.then((d) => d.ip)
.catch(() => null);
const context = createBrowserContext({ ip });
const result = await detectLanguage(context);
console.log(result.lang); // 'ar' (if IP maps to Egypt)
console.log(result.detectedBy); // 'VISITOR_GEO'You can also use your own backend endpoint:
const ip = await fetch("/api/my-ip").then((r) => r.text()).catch(() => null);
const context = createBrowserContext({ ip, userLang: decoded?.lang });When the user is logged in and you have a JWT token:
import { detectLanguage, createBrowserContext, DetectionStage } from "@posty5/lang-detect";
import { jwtDecode } from "jwt-decode";
// Decode the JWT to get the user's saved language
const token = localStorage.getItem("auth_token");
const decoded = token ? jwtDecode<{ lang?: string }>(token) : null;
const context = createBrowserContext({
userLang: decoded?.lang ?? null,
});
const result = await detectLanguage(context, {
stages: [
DetectionStage.PATH_LANG,
DetectionStage.QUERY_LANG,
DetectionStage.USER_LANG, // JWT language has high priority
DetectionStage.COOKIE,
DetectionStage.ACCEPT_LANGUAGE,
DetectionStage.DEFAULT,
],
});// src/app/core/services/browser-language.service.ts
import { Injectable } from "@angular/core";
import { detectLanguage, createBrowserContext, DetectionStage, IDetectionResult } from "@posty5/lang-detect";
@Injectable({ providedIn: "root" })
export class BrowserLanguageService {
private _result: IDetectionResult | null = null;
/** Detect language from the current browser environment */
async detect(userLang?: string | null): Promise<IDetectionResult> {
const context = createBrowserContext({ userLang });
this._result = await detectLanguage(context, {
supportedLanguages: ["en", "ar", "fr", "de", "es"],
stages: [DetectionStage.PATH_LANG, DetectionStage.QUERY_LANG, DetectionStage.USER_LANG, DetectionStage.COOKIE, DetectionStage.ACCEPT_LANGUAGE, DetectionStage.DEFAULT],
});
return this._result;
}
get lang(): string {
return this._result?.lang ?? "en";
}
get isRTL(): boolean {
return this._result?.isRTL ?? false;
}
get detectedBy(): string {
return this._result?.detectedBy ?? "DEFAULT";
}
}Usage in a component:
@Component({
/* ... */
})
export class AppComponent implements OnInit {
private langService = inject(BrowserLanguageService);
private authService = inject(AuthService);
async ngOnInit() {
const userLang = this.authService.currentUser?.lang ?? null;
const result = await this.langService.detect(userLang);
console.log("Language:", result.lang, "from:", result.detectedBy);
}
}Use detectLanguage() directly without Express — works in any Node.js or TypeScript context:
import { detectLanguage, IDetectionContext, DetectionStage } from "@posty5/lang-detect";
// Build context manually
const context: IDetectionContext = {
path: "/ar/dashboard",
queryParams: {},
cookies: { lang: "en" },
headers: { "accept-language": "ar,en;q=0.9" },
userLang: "ar", // from your user database
ip: "41.33.0.1", // from your request
};
const result = await detectLanguage(context, {
supportedLanguages: ["en", "ar", "fr"],
stages: [DetectionStage.USER_LANG, DetectionStage.PATH_LANG, DetectionStage.COOKIE, DetectionStage.DEFAULT],
});
// result.lang = 'ar'
// result.detectedBy = 'USER_LANG'
// result.isRTL = true15 languages out of the box:
| Code | Language | RTL |
|---|---|---|
ar |
Arabic | Yes |
en |
English | No |
hi |
Hindi | No |
es |
Spanish | No |
zh |
Chinese | No |
bn |
Bengali | No |
pt |
Portuguese | No |
ru |
Russian | No |
fr |
French | No |
ur |
Urdu | Yes |
de |
German | No |
it |
Italian | No |
ja |
Japanese | No |
ko |
Korean | No |
tr |
Turkish | No |
Override with supportedLanguages: ['en', 'ar', 'fr'].
45+ country-to-language mappings built in:
| Countries | Language |
|---|---|
| US, GB, CA, AU, NZ, IE, ZA | English (en) |
| EG, SA, AE, JO, KW, QA, LY, MA, SD, OM, BH, TN, DZ, IQ, LB, SY, YE, PS | Arabic (ar) |
| DE, AT, CH | German (de) |
| FR, BE | French (fr) |
| ES, MX, CO, PE, VE, CL, AR | Spanish (es) |
| IT | Italian (it) |
| PT, BR | Portuguese (pt) |
| RU, BY, KZ | Russian (ru) |
| JP | Japanese (ja) |
| KR | Korean (ko) |
| CN, TW, HK | Chinese (zh) |
| IN | Hindi (hi) |
| BD | Bengali (bn) |
| PK | Urdu (ur) |
| TR | Turkish (tr) |
Override with geoToLang: { US: 'en', EG: 'ar', ... }.
ar (Arabic) and ur (Urdu) are detected as RTL. The isRTL flag is set automatically in every IDetectionResult.
import { langDetectMiddleware, DetectionStage } from "@posty5/lang-detect";
app.use(
langDetectMiddleware({
// Only support these languages
supportedLanguages: ["en", "ar", "fr", "de", "es", "tr"],
// Custom detection order
stages: [
DetectionStage.PATH_LANG,
DetectionStage.QUERY_LANG,
DetectionStage.COOKIE,
DetectionStage.USER_LANG,
DetectionStage.GEO_PATH,
DetectionStage.ACCEPT_LANGUAGE,
// DEFAULT auto-appended
],
// Custom country → language map
geoToLang: {
US: "en",
GB: "en",
CA: "en",
EG: "ar",
SA: "ar",
AE: "ar",
DE: "de",
FR: "fr",
ES: "es",
TR: "tr",
},
// Cookie configuration
cookieKeys: ["user_lang", "lang"],
setCookie: true,
cookieOptions: {
maxAge: 90 * 24 * 60 * 60 * 1000, // 90 days
httpOnly: false,
sameSite: "lax",
secure: true,
},
// Query string keys
queryLangKeys: ["lang", "language"],
geoQueryKeys: ["locale", "country"],
// URL path segment (0 = first segment after /)
pathSegmentIndex: 0,
// Default fallback
defaultLanguage: "en",
// IP → country resolver
geoDetector: async (ip) => {
const res = await fetch(`https://your-api.com/geo?ip=${ip}`);
const data = await res.json();
return data.country ?? null;
},
// Extract logged-in user language
userLangResolver: (req, res) => res.locals.user?.lang ?? null,
}),
);Run only one detection stage:
const result = await detectLanguage(context, {
stages: [DetectionStage.COOKIE],
});
// Only checks cookie. If no cookie → falls back to DEFAULT (auto-appended).Extend detection with your own strategy class:
import { IDetectionStrategy, IDetectionContext, IResolvedConfig, DetectionStage, getStrategy } from "@posty5/lang-detect";
// Example: detect from a custom header
class CustomHeaderStrategy implements IDetectionStrategy {
readonly stage = DetectionStage.COOKIE; // reuse an existing stage slot
detect(context: IDetectionContext, config: IResolvedConfig): string | null {
const headerLang = context.headers["x-user-language"];
if (headerLang && config.supportedLanguages.includes(headerLang.toLowerCase())) {
return headerLang.toLowerCase();
}
return null;
}
}Override the default mapping entirely:
const result = await detectLanguage(context, {
geoToLang: {
US: "en",
MX: "es",
BR: "pt",
EG: "ar",
// ... your custom mappings
},
});Or extend the defaults:
import { DEFAULT_GEO_TO_LANG } from "@posty5/lang-detect";
const result = await detectLanguage(context, {
geoToLang: {
...DEFAULT_GEO_TO_LANG,
// Add or override
FI: "fi",
SE: "sv",
},
supportedLanguages: [...DEFAULT_SUPPORTED_LANGUAGES, "fi", "sv"],
});detectLanguage(context, config)
│
├─ 1. Merge user config with defaults (resolveConfig)
│
├─ 2. For each stage in config.stages (in order):
│ │
│ ├─ Get strategy instance from registry
│ ├─ Call strategy.detect(context, resolvedConfig)
│ │
│ ├─ If result is non-null AND is in supportedLanguages:
│ │ └─ RETURN { lang, detectedBy: stage, isRTL }
│ │
│ └─ If result is null:
│ └─ Continue to next stage
│
└─ 3. If no stage matched (shouldn't happen — DEFAULT always matches):
└─ Return { lang: defaultLanguage, detectedBy: DEFAULT, isRTL }
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Type check
npm run typecheck
# Build
npm run buildnpx ts-node tests/test-app.tsThen in another terminal:
# Path language
curl http://localhost:3456/en/page
# → {"lang":"en","detectedBy":"PATH_LANG","isRTL":false}
# Query string
curl http://localhost:3456/page?lang=ar
# → {"lang":"ar","detectedBy":"QUERY_LANG","isRTL":true}
# Geo path
curl http://localhost:3456/us/page
# → {"lang":"en","detectedBy":"GEO_PATH","isRTL":false}
# Geo query
curl "http://localhost:3456/page?locale=eg"
# → {"lang":"ar","detectedBy":"GEO_QUERY","isRTL":true}
# Cookie
curl -H "Cookie: lang=fr" http://localhost:3456/page
# → {"lang":"fr","detectedBy":"COOKIE","isRTL":false}
# Accept-Language header
curl -H "Accept-Language: es,en;q=0.9" http://localhost:3456/page
# → {"lang":"es","detectedBy":"ACCEPT_LANGUAGE","isRTL":false}
# Default fallback
curl http://localhost:3456/page
# → {"lang":"en","detectedBy":"DEFAULT","isRTL":false}- Node.js: >= 16.0.0
- TypeScript: Full type definitions included
- Browser: No native dependencies required
- Website: https://posty5.com
- Dashboard: https://studio.posty5.com
- npm: https://www.npmjs.com/package/@posty5/lang-detect
- GitHub: https://github.com/nicekid1/Posty5-WEB
- Support: https://posty5.com/contact-us
- Contact Us: https://posty5.com/contact-us
- GitHub Issues: Report bugs or request features
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes
- Run tests:
npm test - Type check:
npm run typecheck - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
MIT © Posty5
Made with ❤️ by Posty5