Skip to content

Posty5/lang-detect

Repository files navigation

npm version build tests bundle size license

@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?

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-detect

Express (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-detect

Optional peer dependency for IP-based geo detection (server-only):

npm install geoip-country

If you don't install geoip-country, the VISITOR_GEO stage will be skipped unless you provide a custom geoDetector function.


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/dashboarden Yes
2 QUERY_LANG Language in query string ?lang=arar Yes
3 GEO_PATH Geo code in URL path → language /us/trendsen Yes
4 GEO_QUERY Geo code in query string → language ?locale=egar Yes
5 COOKIE Language stored in cookie Cookie lang=frfr 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: eses Yes (adapted)
9 DEFAULT Fallback — → en Yes

Tip: Pass your own stages array 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.js

API 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;
  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.


createBrowserContext(options?)

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.


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 DEFAULT from 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-country

No extra config needed — the VISITOR_GEO stage auto-detects geoip-country:

app.use(langDetectMiddleware());
// VISITOR_GEO will use geoip-country automatically if installed

Option 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); // false

Tip: To enable VISITOR_GEO in the browser, pass an IP address via options.ip (see With Custom IP below).

With Custom IP (Browser)

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 });

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 = true

Configuration 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 build

Manual Testing with Test App

npx ts-node tests/test-app.ts

Then 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


🆘 Support


🤝 Contributing

Contributions are welcome! Please follow these steps:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Make your changes
  4. Run tests: npm test
  5. Type check: npm run typecheck
  6. Commit your changes: git commit -m 'Add amazing feature'
  7. Push to the branch: git push origin feature/amazing-feature
  8. Open a Pull Request

📄 License

MIT © Posty5


Made with ❤️ by Posty5

About

Unified language detection for Node.js, Angular SSR, and browser — strategy-based with configurable priority stages

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors