| name | internationalization-localization |
| description | This skill should be used when adding multi-language support or expanding to international markets - covers i18n setup with next-intl or react-i18next, translation management, RTL support, locale handling, currency formatting, and cultural adaptation for global applications. |
Internationalization & Localization
Overview
Build applications that work globally. This skill teaches systematic approaches to multi-language support, cultural adaptation, and international expansion.
Core principle: i18n is infrastructure. Build it early or refactor extensively later.
When to Use
Use when:
- Expanding to new markets
- Adding multi-language support
- Building for international audience from start
- Localizing existing application
- Handling currencies, dates, numbers across locales
i18n vs l10n
Internationalization (i18n):
- Engineering work (code infrastructure)
- Enable translation without code changes
- Date/time/currency formatting
- Character encoding (UTF-8)
- One-time setup
Localization (l10n):
- Content work (actual translations)
- Cultural adaptation
- Region-specific assets
- Ongoing per-language
Next.js i18n Setup (Recommended)
Installation
npm install next-intl
Configuration
// i18n.config.ts
export const locales = ['en', 'es', 'fr', 'de', 'ja'] as const
export const defaultLocale = 'en' as const
export type Locale = (typeof locales)[number]
Directory Structure
messages/
├── en.json (English)
├── es.json (Spanish)
├── fr.json (French)
├── de.json (German)
└── ja.json (Japanese)
Translation Files
// messages/en.json
{
"common": {
"welcome": "Welcome",
"signIn": "Sign In",
"signUp": "Sign Up"
},
"dashboard": {
"title": "Dashboard",
"greeting": "Hello, {name}!",
"stats": {
"users": "{count, plural, one {# user} other {# users}}"
}
}
}
// messages/es.json
{
"common": {
"welcome": "Bienvenido",
"signIn": "Iniciar Sesión",
"signUp": "Registrarse"
},
"dashboard": {
"title": "Panel",
"greeting": "¡Hola, {name}!",
"stats": {
"users": "{count, plural, one {# usuario} other {# usuarios}}"
}
}
}
Usage in Components
import {useTranslations} from 'next-intl'
export default function Dashboard() {
const t = useTranslations('dashboard')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('greeting', {name: 'John'})}</p>
<p>{t('stats.users', {count: 1547})}</p>
</div>
)
}
Formatting Patterns
Dates
import {useFormatter} from 'next-intl'
const format = useFormatter()
// Automatic locale formatting
format.dateTime(new Date(), {
year: 'numeric',
month: 'long',
day: 'numeric'
})
// en: January 15, 2025
// es: 15 de enero de 2025
// ja: 2025年1月15日
Numbers
// Numbers with separators
format.number(1234567)
// en: 1,234,567
// de: 1.234.567
// fr: 1 234 567
// Percentages
format.number(0.45, {style: 'percent'})
// 45%
// Compact notation
format.number(1200000, {notation: 'compact'})
// 1.2M
Currency
format.number(99.99, {
style: 'currency',
currency: 'USD'
})
// en-US: $99.99
// en-GB: US$99.99
// ja-JP: $99.99
// de-DE: 99,99 $
// Dynamic currency
format.number(price, {
style: 'currency',
currency: userCurrency // 'EUR', 'GBP', 'JPY', etc.
})
RTL (Right-to-Left) Support
RTL languages:
- Arabic (ar)
- Hebrew (he)
- Persian (fa)
- Urdu (ur)
CSS for RTL
/* Use logical properties */
.element {
margin-inline-start: 16px; /* margin-left in LTR, margin-right in RTL */
margin-inline-end: 8px;
padding-inline: 12px;
}
/* Avoid directional properties */
.bad {
margin-left: 16px; /* Doesn't flip in RTL */
}
Automatic RTL
// Set dir attribute based on locale
<html lang={locale} dir={isRTL(locale) ? 'rtl' : 'ltr'}>
function isRTL(locale: string) {
return ['ar', 'he', 'fa', 'ur'].includes(locale)
}
Tailwind RTL
// Tailwind handles RTL automatically with logical properties
<div className="ms-4"> // margin-start (left in LTR, right in RTL)
<div className="me-4"> // margin-end
<div className="ps-4"> // padding-start
<div className="pe-4"> // padding-end
Pluralization
{
"items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
t('items', {count: 0}) // "No items"
t('items', {count: 1}) // "1 item"
t('items', {count: 5}) // "5 items"
Translation Management
Workflow
Extract strings
- Move all user-facing text to translation files
- Use
t('key')everywhere
Send for translation
- Export JSON files
- Hire native speakers (not Google Translate)
- Professional services: Lokalise, Phrase, Crowdin
Import translations
- Add translated JSON files
- Test in each language
Continuous updates
- New features → new strings
- Track missing translations
- Update regularly
Translation Keys Organization
{
"pages": {
"home": {
"title": "Welcome",
"hero": "Start building today"
},
"about": {
"title": "About Us"
}
},
"components": {
"button": {
"submit": "Submit",
"cancel": "Cancel"
}
},
"errors": {
"required": "This field is required",
"invalid_email": "Invalid email address"
}
}
Locale Detection
Automatic Detection
// Browser language
const browserLocale = navigator.language // 'en-US', 'es-MX', etc.
// Extract language code
const language = browserLocale.split('-')[0] // 'en', 'es'
// Set initial locale
if (locales.includes(language as Locale)) {
setLocale(language as Locale)
}
User Preference
// Save user choice
localStorage.setItem('locale', selectedLocale)
// Load on app start
const savedLocale = localStorage.getItem('locale') as Locale
if (savedLocale) {
setLocale(savedLocale)
}
Language Switcher Component
import {useLocale} from 'next-intl'
import {useRouter, usePathname} from 'next/navigation'
function LanguageSwitcher() {
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
const changeLanguage = (newLocale: Locale) => {
router.replace(pathname, {locale: newLocale})
}
return (
<Select value={locale} onValueChange={changeLanguage}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="es">Español</SelectItem>
<SelectItem value="fr">Français</SelectItem>
<SelectItem value="de">Deutsch</SelectItem>
</SelectContent>
</Select>
)
}
Cultural Adaptation
Beyond Translation
Adapt for culture:
- Images: Show diverse people, culturally appropriate imagery
- Examples: Use local names, scenarios, references
- Colors: Red = danger (Western), luck (China)
- Symbols: Check marks, hand gestures vary
- Formats: Dates, phone numbers, addresses differ
Market-Specific Content
Don't just translate:
- Pricing (local currency, local pricing strategy)
- Support (local hours, language)
- Legal (local terms, privacy laws)
- Payment methods (local popular methods)
Resources
- next-intl: next-intl-docs.vercel.app
- Format.js (Intl API): formatjs.io
- Translation services: Lokalise, Phrase, Crowdin
i18n is investment in growth. Build it early, expand globally easily.