@posty5/lang-detect
v1.0.0
Published
Unified language detection for Node.js, Angular SSR, and browser — strategy-based with configurable priority stages
Maintainers
Readme
@posty5/lang-detect
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.
Table of Contents
- 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
Why This Package?
| 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 |
Quick Start
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'Installation
# 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.
Detection Stages
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 | Server only |
| 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.
Package Structure
@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.jsAPI Reference
detectLanguage(context, config?)
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
// }langDetectMiddleware(config?)
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).
createExpressContext(req, options?)
Creates an IDetectionContext from an Express Request object.
function createExpressContext(req: Request, options?: { userLang?: string | null }): IDetectionContext;Extracts path, query, cookies, headers, and ip (from x-forwarded-for, x-real-ip, or req.ip).
createBrowserContext(options?)
Creates an IDetectionContext from the browser environment.
function createBrowserContext(options?: { userLang?: string | null }): IDetectionContext;Reads window.location, document.cookie, and navigator.languages. IP is null (server-only stages are skipped).
ILangDetectConfig
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' }
}IDetectionContext
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
}IDetectionResult
interface IDetectionResult {
lang: string; // 'en', 'ar', 'fr', etc.
detectedBy: DetectionStage; // Which stage detected the language
isRTL: boolean; // true for 'ar' and 'ur'
}DetectionStage enum
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",
}Usage Guide: Node.js / Express
Basic Express Middleware
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 |
Custom Priority Order
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.
Logged-in User Language (Server)
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;
}
},
}),
);IP-based Geo Detection
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();
});Custom Cookie & Query Keys
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"],
}),
);Disable Cookie Persistence
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",
},
}),
);Usage Guide: Angular SSR
For Angular SSR projects, the language is detected on the server and transferred to the browser via Angular's TransferState.
Step 1 — Express Middleware in server.ts
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,
}),
);Step 2 — Pass Language to Angular via REQUEST_CONTEXT
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);
});Step 3 — Angular Service with TransferState
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);
}
}
}Step 4 — Use in Components
// 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);
}SSR Flow Diagram
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)Usage Guide: Angular (Browser-Only)
For Angular apps without SSR, or when you need to re-detect language on the client side.
Basic Browser Detection
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); // falseNote:
VISITOR_GEO(IP detection) is automatically skipped in browser — no IP is available.
With JWT Token (Logged-in User)
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,
],
});Angular Service Example
// 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);
}
}Usage Guide: Any TypeScript / Node.js Service
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 = trueConfiguration Reference
Supported Languages (Default)
15 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'].
Geo-to-Language Map (Default)
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', ... }.
RTL Languages
ar (Arabic) and ur (Urdu) are detected as RTL. The isRTL flag is set automatically in every IDetectionResult.
Full Config Example
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,
}),
);Advanced Usage
Single Stage Only
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).Custom Strategy
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;
}
}Custom Geo-to-Language Map
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"],
});How Detection Works
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 }Testing
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Type check
npm run typecheck
# Build
npm run buildManual Testing with Test App
npx 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}💻 Requirements
- Node.js: >= 16.0.0
- TypeScript: Full type definitions included
- Browser: No native dependencies required
📖 Resources
- 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
🆘 Support
- Contact Us: https://posty5.com/contact-us
- GitHub Issues: Report bugs or request features
🤝 Contributing
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
📄 License
MIT © Posty5
