Internationalization and localization for React/Next.js applications. Includes translation file management, locale fallbacks, and pluralization support.
Complete toolkit for adding multi-language support to React and Next.js applications.
This skill provides:
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,
}));
├── messages/
│ ├── en.json
│ ├── tr.json
│ └── de.json
├── i18n.ts
├── middleware.ts
└── app/
└── [locale]/
├── layout.tsx
└── page.tsx
// 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"
}
}
{
"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}}"
}
}
{
"greeting": "Merhaba {name}!",
"lastLogin": "Son giriş: {date, date, medium}",
"balance": "Bakiye: {amount, number, currency}",
"percentage": "Tamamlanma: {value, number, percent}"
}
// 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>
);
}
// 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>
);
}
// 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>
);
}
#!/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!")
#!/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)
auth.login.button