i18n-l10n

star 0

Internationalization and localization for React/Next.js applications. Includes translation file management, locale fallbacks, and pluralization support.

atakan-urey By atakan-urey schedule Updated 1/29/2026

name: i18n-l10n description: Internationalization and localization for React/Next.js applications. Includes translation file management, locale fallbacks, and pluralization support.

Internationalization & Localization (i18n/L10n)

Complete toolkit for adding multi-language support to React and Next.js applications.

Overview

This skill provides:

  • Translation file management
  • Locale detection and fallbacks
  • Pluralization and formatting
  • Missing translation detection
  • Translation workflow automation

Quick Start

Setup with next-intl (Next.js App Router)

pnpm add next-intl
// i18n.ts
import { getRequestConfig } from 'next-intl/server';

export const locales = ['en', 'tr', 'de'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'tr';

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`./messages/${locale}.json`)).default,
}));

Project Structure

├── messages/
│   ├── en.json
│   ├── tr.json
│   └── de.json
├── i18n.ts
├── middleware.ts
└── app/
    └── [locale]/
        ├── layout.tsx
        └── page.tsx

Translation File Format

Basic Structure

// messages/tr.json
{
  "common": {
    "welcome": "Hoş geldiniz",
    "login": "Giriş Yap",
    "logout": "Çıkış Yap",
    "save": "Kaydet",
    "cancel": "İptal",
    "delete": "Sil",
    "loading": "Yükleniyor...",
    "error": "Bir hata oluştu"
  },
  "auth": {
    "email": "E-posta",
    "password": "Şifre",
    "forgotPassword": "Şifremi Unuttum",
    "signUp": "Kayıt Ol",
    "loginSuccess": "Başarıyla giriş yaptınız",
    "loginError": "E-posta veya şifre hatalı"
  },
  "validation": {
    "required": "Bu alan zorunludur",
    "email": "Geçerli bir e-posta adresi girin",
    "minLength": "En az {min} karakter olmalıdır",
    "maxLength": "En fazla {max} karakter olabilir"
  }
}

Pluralization

{
  "items": {
    "count": "{count, plural, =0 {Öğe yok} one {# öğe} other {# öğe}}",
    "selected": "{count, plural, =0 {Seçili öğe yok} one {# öğe seçildi} other {# öğe seçildi}}"
  },
  "notifications": {
    "unread": "{count, plural, =0 {Okunmamış bildirim yok} one {# okunmamış bildirim} other {# okunmamış bildirim}}"
  }
}

Interpolation

{
  "greeting": "Merhaba {name}!",
  "lastLogin": "Son giriş: {date, date, medium}",
  "balance": "Bakiye: {amount, number, currency}",
  "percentage": "Tamamlanma: {value, number, percent}"
}

Implementation

1. Next.js App Router Setup

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from './i18n';

export default createMiddleware({
  locales,
  defaultLocale,
  localePrefix: 'as-needed',
});

export const config = {
  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
};
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';

export default async function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  const messages = await getMessages();

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

2. Translation Hooks

// hooks/useTranslation.ts
'use client';

import { useTranslations, useFormatter, useLocale } from 'next-intl';

export function useI18n(namespace?: string) {
  const t = useTranslations(namespace);
  const format = useFormatter();
  const locale = useLocale();

  return {
    t,
    locale,
    formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) =>
      format.dateTime(date, options),
    formatNumber: (value: number, options?: Intl.NumberFormatOptions) =>
      format.number(value, options),
    formatCurrency: (value: number, currency = 'TRY') =>
      format.number(value, { style: 'currency', currency }),
    formatRelative: (date: Date) =>
      format.relativeTime(date),
  };
}

// Usage
function MyComponent() {
  const { t, formatDate, formatCurrency } = useI18n('common');

  return (
    <div>
      <h1>{t('welcome')}</h1>
      <p>{formatDate(new Date())}</p>
      <p>{formatCurrency(1500)}</p>
    </div>
  );
}

3. Language Switcher

// components/LanguageSwitcher.tsx
'use client';

import { useLocale } from 'next-intl';
import { useRouter, usePathname } from 'next/navigation';
import { locales, type Locale } from '@/i18n';

const localeNames: Record<Locale, string> = {
  en: 'English',
  tr: 'Türkçe',
  de: 'Deutsch',
};

export function LanguageSwitcher() {
  const locale = useLocale();
  const router = useRouter();
  const pathname = usePathname();

  const switchLocale = (newLocale: Locale) => {
    // Replace locale in pathname
    const newPathname = pathname.replace(`/${locale}`, `/${newLocale}`);
    router.push(newPathname);
  };

  return (
    <select
      value={locale}
      onChange={(e) => switchLocale(e.target.value as Locale)}
      className="border rounded px-2 py-1"
    >
      {locales.map((loc) => (
        <option key={loc} value={loc}>
          {localeNames[loc]}
        </option>
      ))}
    </select>
  );
}

Scripts

Missing Translation Finder

#!/usr/bin/env python3
"""Find missing translations across locale files."""

import json
import sys
from pathlib import Path
from typing import Dict, Set

def flatten_keys(obj: dict, prefix: str = '') -> Set[str]:
    """Flatten nested dict keys."""
    keys = set()
    for key, value in obj.items():
        full_key = f"{prefix}.{key}" if prefix else key
        if isinstance(value, dict):
            keys.update(flatten_keys(value, full_key))
        else:
            keys.add(full_key)
    return keys

def find_missing_translations(messages_dir: str) -> Dict[str, list]:
    """Find missing translations in locale files."""
    path = Path(messages_dir)
    locale_files = list(path.glob('*.json'))

    if not locale_files:
        print(f"No locale files found in {messages_dir}")
        return {}

    # Load all locales
    locales = {}
    for file in locale_files:
        locale = file.stem
        locales[locale] = json.loads(file.read_text())

    # Get all keys from each locale
    all_keys: Dict[str, Set[str]] = {}
    for locale, messages in locales.items():
        all_keys[locale] = flatten_keys(messages)

    # Find union of all keys
    all_possible_keys = set()
    for keys in all_keys.values():
        all_possible_keys.update(keys)

    # Find missing keys for each locale
    missing: Dict[str, list] = {}
    for locale, keys in all_keys.items():
        missing_keys = all_possible_keys - keys
        if missing_keys:
            missing[locale] = sorted(missing_keys)

    return missing

if __name__ == '__main__':
    dir_path = sys.argv[1] if len(sys.argv) > 1 else 'messages'
    result = find_missing_translations(dir_path)

    if result:
        print("Missing translations found:\n")
        for locale, keys in result.items():
            print(f"{locale}:")
            for key in keys:
                print(f"  - {key}")
            print()
    else:
        print("All translations are complete!")

Translation Sync Script

#!/usr/bin/env python3
"""Sync translations from base locale to others."""

import json
import sys
from pathlib import Path
from typing import Dict, Any

def deep_merge(base: Dict, target: Dict) -> Dict:
    """Merge base into target, keeping target values where they exist."""
    result = target.copy()

    for key, value in base.items():
        if key not in result:
            # Add missing key with placeholder
            if isinstance(value, dict):
                result[key] = deep_merge(value, {})
            else:
                result[key] = f"[TRANSLATE] {value}"
        elif isinstance(value, dict) and isinstance(result[key], dict):
            result[key] = deep_merge(value, result[key])

    return result

def sync_translations(messages_dir: str, base_locale: str = 'en'):
    """Sync translations from base locale to all others."""
    path = Path(messages_dir)
    base_file = path / f"{base_locale}.json"

    if not base_file.exists():
        print(f"Base locale file not found: {base_file}")
        return

    base_messages = json.loads(base_file.read_text())

    for locale_file in path.glob('*.json'):
        if locale_file.stem == base_locale:
            continue

        target_messages = json.loads(locale_file.read_text())
        synced = deep_merge(base_messages, target_messages)

        # Save with proper formatting
        locale_file.write_text(
            json.dumps(synced, ensure_ascii=False, indent=2) + '\n'
        )
        print(f"Synced: {locale_file.name}")

if __name__ == '__main__':
    dir_path = sys.argv[1] if len(sys.argv) > 1 else 'messages'
    base = sys.argv[2] if len(sys.argv) > 2 else 'en'
    sync_translations(dir_path, base)

Best Practices

Translation Keys

  • Use namespaced keys: auth.login.button
  • Keep keys semantic, not content-based
  • Use camelCase for consistency
  • Group related translations

Formatting

  • Use ICU message format for pluralization
  • Let formatters handle dates/numbers
  • Don't concatenate translated strings
  • Provide context for translators

Performance

  • Split large translation files by page/feature
  • Use lazy loading for non-critical locales
  • Cache formatted values where appropriate

Checklist

Initial Setup

  • Choose i18n library (next-intl, react-i18next)
  • Create locale file structure
  • Set up middleware for locale detection
  • Implement language switcher

For Each Feature

  • Extract all user-facing strings
  • Add keys to base locale file
  • Sync to other locale files
  • Review and translate

Before Release

  • Run missing translation check
  • Test RTL layouts (if applicable)
  • Verify date/number formatting per locale
  • Check string lengths in UI
Install via CLI
npx skills add https://github.com/atakan-urey/ai-finance-bot-v1 --skill i18n-l10n
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator