OmniRoute i18n System: How It Supports 42 Locales and the Translation Workflow

OmniRoute's internationalization system supports 42 locales through a data-driven configuration in config/i18n.json, with a translation workflow that uses deep-merge fallbacks, browser detection, and Next-Intl integration to serve localized content without hard-coding locale lists.

The OmniRoute repository (diegosouzapw/OmniRoute) implements a fully-typed, data-driven internationalization layer capable of serving UI translations across 42 distinct language locales. Unlike systems that hard-code supported languages in multiple files, OmniRoute centralizes locale definitions in a single JSON configuration, enabling automatic propagation of changes throughout the TypeScript codebase. This architecture ensures that adding or removing a locale requires updates to only one file, while the runtime handles fallback chains, missing key detection, and right-to-left layout switching automatically.

Core Architecture: Four Modules Powering 42 Locales

The OmniRoute i18n system relies on four tightly integrated modules that separate configuration, type safety, request resolution, and browser detection.

config/i18n.json as the Source of Truth

The file config/i18n.json serves as the master source of truth, containing 42 entries that define each locale's code, label, name, native representation, English description, and flag icon. This JSON file also specifies the default locale and lists all rtl (right-to-left) languages. By maintaining a single canonical list, the system guarantees consistency across the application without requiring scattered updates when locales change.

src/i18n/config.ts Type Safety Layer

The src/i18n/config.ts module acts as a typed adapter that imports the JSON configuration and exposes four critical constants: LOCALES (a readonly string[]), DEFAULT_LOCALE, RTL_LOCALES, and LANGUAGES. Because this module never hard-codes the locale list, any modification to config/i18n.json immediately reflects in the TypeScript type system. This prevents runtime errors and ensures that the rest of the codebase always references the current, accurate list of 42 supported locales.

src/i18n/request.ts Request Resolution

The src/i18n/request.ts file implements the Next-Intl request-level resolver through the getRequestConfig pattern. This module performs five critical operations: (a) reading the locale from the NEXT_LOCALE cookie or the x-locale HTTP header, (b) validating the locale against the supported list and falling back to the default if invalid, (c) dynamically importing the corresponding message bundle from ./messages/${locale}.json, (d) executing a two-stage deep-merge algorithm to fill missing keys from English (en), and (e) returning the final { locale, messages } object for page rendering.

src/i18n/detectBrowserLocale.ts Browser Detection

The src/i18n/detectBrowserLocale.ts utility provides a pure function with no DOM dependencies, enabling reuse on both client and server. It accepts an array of language strings (typically navigator.languages) and applies a three-tier matching strategy: exact match (e.g., en-USen), special-case mapping for Hong Kong and Macao (zh-HK / zh-MOzh-TW), and prefix matching (e.g., fr-CAfr). The function returns the first supported locale or null, allowing the application to auto-select a language before the user explicitly sets a preference cookie.

Step-by-Step Translation Workflow

The translation workflow operates through a deterministic pipeline that handles locale detection, message loading, and graceful degradation when translations are incomplete.

Bootstrapping the Locale List

During the build process, the script scripts/i18n/run-translation.mjs reads config/i18n.json to validate the locale structure. The src/i18n/config.ts module then re-exports the array as LOCALES, making the list available to routing logic, middleware, and UI components. This single-source approach guarantees that adding a locale to the JSON file automatically enables it throughout the application without code changes.

Detecting the User's Preferred Locale

When a request arrives without a NEXT_LOCALE cookie, the system checks for the x-locale header. If neither exists, the client-side code can invoke detectBrowserLocale(navigator.languages, LOCALES) to determine the best match based on the user's browser preferences. Once identified, the chosen locale is stored in the NEXT_LOCALE cookie with a one-year max-age, ensuring subsequent requests use the cached value.

Loading and Merging Message Bundles

The src/i18n/request.ts module dynamically imports JSON files from src/i18n/messages/ based on the resolved locale. Each file contains plain key-value maps representing translation strings for that language. The system implements a two-stage fallback merge: first, it performs a deep merge of the target locale with the generic English (en) bundle to ensure every key exists; second, it performs a shallow top-level merge to add any new namespaces that exist only in English, allowing non-English locales to display English strings for newly added features until translations are provided.

Handling Missing Translations with Placeholders

The translation sync script tags untranslated strings with the __MISSING__: sentinel prefix. Within src/i18n/request.ts, the deepMergeFallback() function (lines 20-27) treats these placeholders as missing values, causing the fallback English translation to win during the merge process. This mechanism prevents empty strings or broken UI elements when translation coverage is incomplete.

RTL Language Support

The src/i18n/config.ts module exports RTL_LOCALES derived from the rtl array in config/i18n.json. This list includes Arabic, Persian, Hebrew, and Urdu among the 42 locales. UI components consume this constant to conditionally apply right-to-left layout direction, ensuring proper text rendering and component alignment for supported languages.

Implementing i18n in Practice

Detecting Browser Locale on First Visit

Use the detectBrowserLocale utility to automatically select a language when a user first lands on the site:

import { detectBrowserLocale } from '@/i18n/detectBrowserLocale';
import { LOCALES } from '@/i18n/config';

const userLangs = navigator.languages; // e.g., ['pt-BR', 'en-US']
const chosen = detectBrowserLocale(userLangs, LOCALES) ?? 'en';

document.cookie = `NEXT_LOCALE=${chosen}; path=/; max-age=31536000`;

Using Translations in React Components

Access localized strings through the Next-Intl hooks after the request configuration has loaded the messages:

import { useTranslations } from 'next-intl';

export function Header() {
  const t = useTranslations('header');
  
  return (
    <header>
      <h1>{t('title')}</h1>
      <p>{t('subtitle')}</p>
    </header>
  );
}

Adding a New Locale to the System

To extend support beyond the current 42 locales:

  1. Append a new entry to config/i18n.json with the locale code, label, name, and flag properties.
  2. Execute npm run i18n:sync to generate src/i18n/messages/<code>.json populated with __MISSING__: placeholders.
  3. Provide translations for each key; the runtime will automatically serve the new locale without modifying TypeScript code.

Summary

  • Single source of truth: The config/i18n.json file centrally defines all 42 supported locales, preventing drift between configuration and code.
  • Type-safe propagation: src/i18n/config.ts exposes locale arrays as readonly constants, ensuring TypeScript catches invalid locale references at compile time.
  • Intelligent fallback: The request resolver in src/i18n/request.ts deep-merges missing keys from English and handles __MISSING__: placeholders to prevent UI breakage.
  • Browser-aware detection: The pure detectBrowserLocale function implements exact, special-case, and prefix matching to select the optimal locale from navigator.languages.
  • Zero-code locale addition: New locales require only JSON configuration and message file creation, with the build pipeline and type system adapting automatically.

Frequently Asked Questions

How does OmniRoute handle missing translations for supported locales?

When a translation key is missing from a locale's message bundle, the deepMergeFallback() function in src/i18n/request.ts detects the __MISSING__: placeholder sentinel and replaces it with the corresponding English value. This two-stage deep-merge process ensures that users always see content in English rather than empty strings or error states when translations are incomplete.

Can the locale detection work on the server side for initial page loads?

Yes, the detectBrowserLocale function is designed as a pure utility without DOM dependencies, allowing it to run in server-side rendering contexts. However, for initial loads without a NEXT_LOCALE cookie, the system typically relies on the x-locale header or falls back to the default locale, with client-side detection available to set the cookie for subsequent requests.

What is the process for adding a new language to the 42 existing locales?

To add a new language, developers append the locale metadata to config/i18n.json, run npm run i18n:sync to generate a placeholder message file in src/i18n/messages/, and then provide the actual translations. The src/i18n/config.ts adapter automatically picks up the new locale, making it available throughout the application without modifying any TypeScript code or routing logic.

How does OmniRoute determine text direction for right-to-left languages?

The system reads the rtl array from config/i18n.json and exposes it as RTL_LOCALES in src/i18n/config.ts. UI components check this list to conditionally apply RTL layout direction. This covers Arabic, Persian, Hebrew, Urdu, and any other right-to-left languages defined in the configuration, ensuring proper text rendering across all 42 locales.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →