Internationalization is more than translating strings. Pluralization rules differ across languages (English: 1 apple / 2 apples; Russian: 4 separate cases). Dates render differently. Numbers use different separators. Right-to-left languages flip layouts. Claude Code extracts strings from existing code, sets up ICU message format for complex cases, and generates locale-correct formatting.
Setup: next-intl for Next.js
CLAUDE.md for i18n Projects
## Internationalization Stack
- next-intl for Next.js App Router
- ICU message format (handles plurals, gender, select, dates)
- Message files: messages/{locale}.json
- Supported locales: en (default), es, fr, de, ja, ar (RTL)
- Locale routing: /en/..., /es/..., etc. (locale in URL path)
- Missing translations: fall back to English (never show translation keys)
// i18n.ts — locale detection and message loading
import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';
const locales = ['en', 'es', 'fr', 'de', 'ja', 'ar'] as const;
export type Locale = (typeof locales)[number];
export default getRequestConfig(async ({ locale }) => {
if (!locales.includes(locale as Locale)) notFound();
return {
messages: (await import(`./messages/${locale}.json`)).default,
};
});
// messages/en.json
{
"navigation": {
"home": "Home",
"products": "Products",
"cart": "Cart ({count, number})"
},
"product": {
"title": "{name}",
"price": "{price, number, ::currency/USD}",
"addToCart": "Add to Cart",
"outOfStock": "Out of Stock",
"stockRemaining": "{count, plural, =0 {Out of stock} one {Last {count} item} other {{count} items left}}"
},
"checkout": {
"orderTotal": "Order Total: {total, number, ::currency/USD}",
"itemCount": "You have {count, plural, one {# item} other {# items}} in your cart",
"deliveryDate": "Estimated delivery: {date, date, long}"
}
}
// messages/ar.json — Arabic (RTL, different plural rules: 6 forms!)
{
"product": {
"stockRemaining": "{count, plural, =0 {نفذ المخزون} one {آخر قطعة} two {آخر قطعتان} few {آخر {count} قطع} many {{count} قطعة} other {{count} قطعة}}"
},
"checkout": {
"deliveryDate": "التسليم المتوقع: {date, date, long}"
}
}
Components with Translation
Translate this product listing component.
Extract all text strings and replace with translation keys.
// app/[locale]/components/ProductCard.tsx
'use client';
import { useTranslations, useFormatter } from 'next-intl';
import type { Product } from '@/types';
// Before: hardcoded English
// function ProductCard({ product }) {
// return (
// <div>
// <h2>{product.name}</h2>
// <p>${product.price}</p>
// {product.stock === 0 && <span>Out of stock</span>}
// </div>
// );
// }
// After: internationalized
export function ProductCard({ product }: { product: Product }) {
const t = useTranslations('product');
const format = useFormatter();
return (
<div>
<h2>{product.name}</h2>
{/* Currency: respects locale (€1.234,56 in Germany; $1,234.56 in US) */}
<p>
{format.number(product.priceCents / 100, {
style: 'currency',
currency: product.currency ?? 'USD',
})}
</p>
{/* ICU select: handles all plural cases */}
<span className={product.stock === 0 ? 'text-red' : 'text-green'}>
{t('stockRemaining', { count: product.stock })}
</span>
{/* Formatted date */}
<p>
{t('deliveryDate', {
date: new Date(product.estimatedDelivery),
})}
</p>
</div>
);
}
Extracting Strings from Existing Code
We have a large React app with hardcoded English strings everywhere.
Extract all visible text into translation keys.
Claude Code reads your components and generates the translation files:
// Input: existing component with hardcoded strings
function CheckoutSummary({ items, total }) {
return (
<div>
<h2>Order Summary</h2>
<p>You have {items.length} items in your cart</p>
<p>Subtotal: ${(total / 100).toFixed(2)}</p>
{items.length === 0 && <p>Your cart is empty. Start shopping!</p>}
<button>Proceed to Payment</button>
</div>
);
}
// Output: internationalized version
function CheckoutSummary({ items, total, currency = 'USD' }) {
const t = useTranslations('checkout');
const format = useFormatter();
return (
<div>
<h2>{t('orderSummaryTitle')}</h2>
<p>{t('itemCount', { count: items.length })}</p>
<p>{t('subtotal', { amount: format.number(total / 100, { style: 'currency', currency }) })}</p>
{items.length === 0 && <p>{t('emptyCart')}</p>}
<button>{t('proceedToPayment')}</button>
</div>
);
}
Generated strings to add to messages/en.json:
{
"checkout": {
"orderSummaryTitle": "Order Summary",
"itemCount": "{count, plural, one {You have # item in your cart} other {You have # items in your cart}}",
"subtotal": "Subtotal: {amount}",
"emptyCart": "Your cart is empty. Start shopping!",
"proceedToPayment": "Proceed to Payment"
}
}
RTL Layout Support
We need to support Arabic and Hebrew — both RTL.
How do we handle layout mirroring without duplicating CSS?
// app/[locale]/layout.tsx — set dir attribute based on locale
import { getLocale } from 'next-intl/server';
const RTL_LOCALES = ['ar', 'he', 'fa', 'ur'];
export default async function RootLayout({ children }) {
const locale = await getLocale();
const dir = RTL_LOCALES.includes(locale) ? 'rtl' : 'ltr';
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
);
}
/* Use logical CSS properties — they flip automatically with dir="rtl" */
/* ❌ Physical properties — don't flip */
.nav { padding-left: 16px; text-align: left; }
/* ✅ Logical properties — flip with RTL automatically */
.nav { padding-inline-start: 16px; text-align: start; }
/* Icons that need to mirror (arrows, chevrons) */
[dir="rtl"] .arrow-icon {
transform: scaleX(-1);
}
Number and Date Formatting
Show dates and numbers in the user's locale format.
// React hook: locale-aware formatting
import { useFormatter, useNow } from 'next-intl';
function OrderStatus({ order }) {
const format = useFormatter();
const now = useNow();
return (
<div>
{/* Currency — locale-appropriate format */}
<p>Total: {format.number(order.totalCents / 100, {
style: 'currency',
currency: order.currency,
})}</p>
{/* en-US: $1,234.56, de-DE: 1.234,56 €, ja-JP: ¥1,235 */}
{/* Relative time */}
<p>Ordered {format.relativeTime(new Date(order.createdAt), now)}</p>
{/* en: "3 hours ago", es: "hace 3 horas", de: "vor 3 Stunden" */}
{/* Absolute date */}
<p>Delivered by {format.dateTime(new Date(order.deliveryDate), {
weekday: 'long',
month: 'long',
day: 'numeric',
})}</p>
{/* en-US: "Wednesday, September 28", ar: "الأربعاء، ٢٨ سبتمبر" */}
{/* List formatting */}
<p>Ships to {format.list(order.availableRegions, { type: 'disjunction' })}</p>
{/* en: "US, UK, or Canada", de: "USA, UK oder Kanada" */}
</div>
);
}
CI: Validate All Translations
How do I prevent missing translation keys from reaching production?
// scripts/validate-translations.ts
import { readdirSync, readFileSync } from 'fs';
import path from 'path';
const messagesDir = path.join(process.cwd(), 'messages');
const locales = readdirSync(messagesDir).map(f => f.replace('.json', ''));
// Load all translation files
const translations = Object.fromEntries(
locales.map(locale => [
locale,
JSON.parse(readFileSync(path.join(messagesDir, `${locale}.json`), 'utf-8'))
])
);
// Get all keys from reference locale (English)
function getAllKeys(obj: object, prefix = ''): string[] {
return Object.entries(obj).flatMap(([key, value]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
return typeof value === 'object' ? getAllKeys(value, fullKey) : [fullKey];
});
}
const referenceKeys = new Set(getAllKeys(translations.en));
let hasErrors = false;
for (const locale of locales.filter(l => l !== 'en')) {
const localeKeys = new Set(getAllKeys(translations[locale]));
const missing = [...referenceKeys].filter(k => !localeKeys.has(k));
const extra = [...localeKeys].filter(k => !referenceKeys.has(k));
if (missing.length > 0) {
console.error(`❌ ${locale}: Missing ${missing.length} keys:`, missing.slice(0, 5));
hasErrors = true;
}
if (extra.length > 0) {
console.warn(`⚠ ${locale}: ${extra.length} extra keys (not in en.json):`, extra.slice(0, 3));
}
}
if (!hasErrors) {
console.log('✅ All translations complete');
} else {
process.exit(1);
}
For building React applications that use these i18n patterns, see the React guide. For Next.js App Router locale routing setup, the Next.js App Router guide covers the middleware configuration. The Claude Skills 360 bundle includes i18n skill sets for extraction, ICU message formatting, and RTL support. Start with the free tier to try translation extraction from existing components.