How to Set Up and Manage i18n with 42 Locales Using next-intl in OmniRoute

OmniRoute provides a complete next-intl integration that generates type-safe locale constants from a single JSON configuration file, enabling zero-code additions of new languages.

Managing internationalization across 42 locales requires a maintainable, type-safe architecture. OmniRoute solves this by centralizing all locale metadata in config/i18n.json and deriving TypeScript constants automatically. This guide walks through the complete setup based on the actual source code implementation in the diegosouzapw/OmniRoute repository.

OmniRoute's i18n Architecture

The system separates configuration from implementation across four layers:

Layer File Purpose
Catalogue config/i18n.json Source of truth for 42 locale definitions
Types src/i18n/config.ts Generated constants: LOCALES, DEFAULT_LOCALE, RTL_LOCALES
Server src/i18n/request.ts getRequestConfig for next-intl server integration
Client src/app/layout.tsx NextIntlClientProvider wrapping the application

This architecture ensures that modifying config/i18n.json automatically updates all type-safe references throughout the codebase on the next build.

Configuring the Locale Catalogue

The 42-locale list lives in a single JSON file without hard-coded TypeScript equivalents.

config/i18n.json Structure

{
  "default": "en",
  "rtl": ["ar", "he", "fa"],
  "locales": [
    {
      "code": "en",
      "label": "English",
      "name": "English",
      "native": "English",
      "english": "English",
      "flag": "🇺🇸"
    },
    {
      "code": "pt",
      "label": "Portuguese",
      "name": "Português",
      "native": "Português",
      "english": "Portuguese",
      "flag": "🇧🇷"
    }
    // ... 40 additional locales
  ]
}

Fields include:

  • code: ISO locale identifier used in URLs and APIs
  • label: Display name in the current locale
  • native: Autonym (name in the language itself)
  • flag: Emoji flag for UI selectors

Generating Type-Safe Locale Constants

In src/i18n/config.ts, OmniRoute transforms the JSON catalogue into compile-time constants:

import config from '../../config/i18n.json';

export const LOCALES = config.locales.map(l => l.code);
// Type: string[] — derived at build time

export const DEFAULT_LOCALE = config.default;
// Type: string

export const RTL_LOCALES = config.rtl;
// Type: string[]

export const LOCALE_COOKIE = 'NEXT_LOCALE';
// Cookie name used by next-intl

Using config.locales.map(l => l.code) at module initialization guarantees that LOCALES (line 26 in src/i18n/config.ts) always reflects the current JSON state. No manual synchronization required.

Setting Up the Server-Side Request Loader

The src/i18n/request.ts file implements next-intl's getRequestConfig to resolve locales per incoming request:

import { getRequestConfig } from 'next-intl/server';
import { LOCALES, DEFAULT_LOCALE } from './config';

export default getRequestConfig({
  locales: LOCALES,
  defaultLocale: DEFAULT_LOCALE,
  timeZone: 'UTC',
  now: new Date()
});

This default export is consumed by Next.js App Router's i18n routing. The configuration:

  • Validates incoming locales against the 42-entry LOCALES array
  • Falls back to DEFAULT_LOCALE for unmatched paths
  • Enables automatic message loading from src/i18n/messages/

Integrating the Client Provider

Root layout setup in src/app/layout.tsx (lines 4-5) establishes the client-side context:

import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getLocale } from 'next-intl/server';

export default async function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  const locale = await getLocale();
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider 
          locale={locale} 
          messages={messages}
          timeZone="UTC"
        >
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

getLocale() respects the priority order: URL segment → NEXT_LOCALE cookie → Accept-Language header → DEFAULT_LOCALE. getMessages() automatically loads the matching JSON file from src/i18n/messages/{locale}.json.

Adding a New Locale (Zero-Code Workflow)

Expanding beyond 42 locales requires only JSON edits:

  1. Update config/i18n.json with the new locale object:
{
  "code": "ko",
  "label": "Korean",
  "name": "Korean",
  "native": "한국어",
  "english": "Korean",
  "flag": "🇰🇷"
}
  1. Create translation file at src/i18n/messages/ko.json:
{
  "metadata": {
    "title": "OmniRoute - 글로벌 물류 플랫폼"
  },
  "navigation": {
    "dashboard": "대시보드",
    "shipments": "배송"
  }
}
  1. Rebuild: npm run build

The LOCALES constant automatically includes "ko"; TypeScript compilation validates all references; the UI instantly supports Korean selection.

Consuming Locales in Components

Access the generated constants and next-intl hooks in React components:

'use client';

import { useLocale, useTranslations } from 'next-intl';
import { LOCALES } from '@/i18n/config';

export default function LanguageSwitcher() {
  const t = useTranslations('language-selector');
  const currentLocale = useLocale();

  return (
    <select 
      value={currentLocale}
      onChange={(e) => {
        // Locale change logic
        document.cookie = `NEXT_LOCALE=${e.target.value}; path=/`;
        window.location.href = `/${e.target.value}`;
      }}
    >
      {LOCALES.map((code) => (
        <option key={code} value={code}>
          {t(`locale.${code}`)}
        </option>
      ))}
    </select>
  );
}

LOCALES from src/i18n/config.ts line 26 provides the complete 42-item array. The t() function references nested keys in the translation files.

Managing Translations at Scale

For 42 locales, OmniRoute organizes messages by feature domain:


src/i18n/messages/
├── en.json      (source reference)
├── pt.json
├── es.json
├── ...
└── _build/      (optional generated files)

Keep English as the authoritative source, then synchronize secondary locales via CI pipelines or translation management platforms. The flat JSON structure allows partial translations—missing keys gracefully fall back through next-intl's default behavior.

Summary

  • Single source of truth: config/i18n.json defines all 42 locales; no scattered hard-coding
  • Type-safe generation: src/i18n/config.ts builds LOCALES, DEFAULT_LOCALE, and RTL_LOCALES at build time
  • Server integration: src/i18n/request.ts provides getRequestConfig for App Router compatibility
  • Client hydration: NextIntlClientProvider in src/app/layout.tsx supplies hooks globally
  • Zero-code scaling: Add locales by editing JSON and creating translation files; TypeScript updates automatically

Frequently Asked Questions

Where does OmniRoute store the list of supported locales?

The 42 locales are defined in config/i18n.json. This JSON file is the only location requiring manual edits when adding or removing languages. The src/i18n/config.ts module imports this file and exports LOCALES as a generated string array derived from config.locales.map(l => l.code).

How does next-intl determine which locale to use for a request?

The resolution order implemented in getLocale() follows: URL path segment first, then the NEXT_LOCALE cookie, then the Accept-Language header, and finally DEFAULT_LOCALE from configuration. This logic executes in src/i18n/request.ts through next-intl's getRequestConfig implementation.

Do I need to modify TypeScript code when adding a new language?

No. Adding a locale requires only updating config/i18n.json and creating the corresponding src/i18n/messages/{code}.json file. Running npm run build regenerates type-safe constants automatically. The LOCALES export in src/i18n/config.ts updates without code changes.

How does OmniRoute handle right-to-left languages?

The rtl array in config/i18n.json specifies which locales require RTL layout. src/i18n/config.ts exports this as RTL_LOCALES. Components can detect RTL mode by checking RTL_LOCALES.includes(currentLocale) and applying directional styles accordingly.

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 →