Internationalization (i18n)
Build globally accessible applications with proper internationalization patterns
Internationalization (i18n)
Internationalization (i18n) is the process of designing your application to work in multiple languages and regions. Companies like Uber, Airbnb, and Netflix operate globally—here's how to build apps that scale internationally.
Why i18n Matters
Business Impact:
- Access global markets
- Improve user experience for non-English speakers
- Legal requirements in some countries
- Competitive advantage
- Increased conversion rates
Common Mistakes:
- Hardcoded strings
- Assuming left-to-right layout
- Ignoring date/number formats
- No fallback strategy
- Loading all translations at once
Core i18n Concepts
1. Locale
A locale identifies a language and region:
en-US: English (United States)en-GB: English (Great Britain)pt-BR: Portuguese (Brazil)es-MX: Spanish (Mexico)ar-SA: Arabic (Saudi Arabia)
2. Translation Keys
// ❌ BAD: Hardcoded text
<button>Submit Form</button>
// ✅ GOOD: Translation keys
<button>{t('form.submit')}</button>
// en.json: { "form": { "submit": "Submit Form" } }
// pt.json: { "form": { "submit": "Enviar Formulário" } }
// ar.json: { "form": { "submit": "إرسال النموذج" } }3. Pluralization
// ❌ BAD: Simple concatenation
`${count} item${count !== 1 ? 's' : ''}`
// ✅ GOOD: Proper pluralization
t('cart.items', { count })
// en: { "one": "{{count}} item", "other": "{{count}} items" }
// ar: { "zero": "لا عناصر", "one": "عنصر واحد", "two": "عنصرين", "few": "{{count}} عناصر", "many": "{{count}} عنصرًا", "other": "{{count}} عنصر" }4. Date/Time Formatting
// ❌ BAD: Manual formatting
const date = `${month}/${day}/${year}`;
// ✅ GOOD: Intl.DateTimeFormat
new Intl.DateTimeFormat('en-US').format(date); // 12/31/2024
new Intl.DateTimeFormat('pt-BR').format(date); // 31/12/2024
new Intl.DateTimeFormat('ja-JP').format(date); // 2024/12/315. Number/Currency Formatting
// ❌ BAD: Manual formatting
const price = `$${amount.toFixed(2)}`;
// ✅ GOOD: Intl.NumberFormat
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(100);
// $100.00
new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(100);
// R$ 100,00
new Intl.NumberFormat('ar-SA', { style: 'currency', currency: 'SAR' }).format(100);
// ١٠٠٫٠٠ ر.س.Popular i18n Libraries
| Library | Best For | Size | Features |
|---|---|---|---|
| next-intl | Next.js apps | Small | Server + Client, Type-safe |
| react-i18next | React apps | Medium | Mature, Full-featured |
| FormatJS (react-intl) | Enterprise | Medium | ICU messages, Polyfills |
| i18next | Any framework | Small | Framework-agnostic |
| Lingui | Performance | Small | Compile-time, Minimal runtime |
Basic Setup (next-intl)
Installation
npm install next-intlConfiguration
// i18n.ts
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default,
}));// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['en', 'pt', 'es', 'ar'],
defaultLocale: 'en',
localePrefix: 'as-needed',
});
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
};Translation Files
// messages/en.json
{
"common": {
"welcome": "Welcome",
"signIn": "Sign In",
"signOut": "Sign Out"
},
"cart": {
"title": "Shopping Cart",
"empty": "Your cart is empty",
"items": {
"one": "{count} item",
"other": "{count} items"
},
"total": "Total: {amount}"
},
"errors": {
"required": "This field is required",
"invalidEmail": "Please enter a valid email"
}
}// messages/pt.json
{
"common": {
"welcome": "Bem-vindo",
"signIn": "Entrar",
"signOut": "Sair"
},
"cart": {
"title": "Carrinho de Compras",
"empty": "Seu carrinho está vazio",
"items": {
"one": "{count} item",
"other": "{count} itens"
},
"total": "Total: {amount}"
},
"errors": {
"required": "Este campo é obrigatório",
"invalidEmail": "Por favor, insira um email válido"
}
}Usage
// app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('common');
return (
<div>
<h1>{t('welcome')}</h1>
<button>{t('signIn')}</button>
</div>
);
}
// With parameters
function Cart({ count }: { count: number }) {
const t = useTranslations('cart');
return (
<div>
<h2>{t('title')}</h2>
<p>{t('items', { count })}</p>
</div>
);
}i18n Architecture Patterns
1. File Structure
app/
├── [locale]/
│ ├── layout.tsx
│ ├── page.tsx
│ └── dashboard/
│ └── page.tsx
│
messages/
├── en/
│ ├── common.json
│ ├── dashboard.json
│ └── errors.json
├── pt/
│ ├── common.json
│ ├── dashboard.json
│ └── errors.json
└── ar/
├── common.json
├── dashboard.json
└── errors.json2. Namespace Organization
// Good: Organize by feature
messages/
├── en/
│ ├── auth.json // Login, signup
│ ├── cart.json // Shopping cart
│ ├── checkout.json // Checkout flow
│ └── common.json // Shared terms
// Usage
const t = useTranslations('auth');
t('login.title');
t('login.emailLabel');3. Shared Translations
// messages/en/common.json
{
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit"
},
"validation": {
"required": "This field is required",
"email": "Invalid email address",
"minLength": "Must be at least {min} characters"
}
}Best Practices
1. Extract Translations
# Use tools to extract translatable strings
npm run extract-translations2. Type-Safe Translations
// next-intl provides automatic type checking
const t = useTranslations('cart');
t('items', { count: 5 }); // ✅ Type-safe
t('nonexistent'); // ❌ TypeScript error3. Locale Switcher
'use client';
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next/navigation';
export function LocaleSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const switchLocale = (newLocale: string) => {
// Replace locale in URL
const newPathname = pathname.replace(`/${locale}`, `/${newLocale}`);
router.push(newPathname);
};
return (
<select value={locale} onChange={(e) => switchLocale(e.target.value)}>
<option value="en">English</option>
<option value="pt">Português</option>
<option value="es">Español</option>
<option value="ar">العربية</option>
</select>
);
}4. SEO Considerations
// app/[locale]/layout.tsx
export async function generateMetadata({ params: { locale } }: Props) {
const t = await getTranslations({ locale, namespace: 'metadata' });
return {
title: t('title'),
description: t('description'),
alternates: {
canonical: `/${locale}`,
languages: {
'en': '/en',
'pt': '/pt',
'es': '/es',
'ar': '/ar',
},
},
};
}Common Pitfalls
❌ Concatenating strings: Breaks in other languages
✅ Use ICU message format
❌ Assuming left-to-right: Arabic, Hebrew are RTL
✅ Test with RTL languages
❌ Hardcoding date formats: Different per locale
✅ Use Intl.DateTimeFormat
❌ Simple plural rules: Languages have complex plurals
✅ Use proper pluralization
❌ Loading all translations: Large bundle size
✅ Lazy load by route
i18n Checklist
- Set up translation files structure
- Implement locale detection and switching
- Add RTL support for Arabic/Hebrew
- Format dates with Intl.DateTimeFormat
- Format numbers/currency with Intl.NumberFormat
- Implement proper pluralization rules
- Lazy load translations by route
- Add translation fallback strategy
- Test with all supported locales
- Set up hreflang tags for SEO
- Implement locale-specific URLs
- Add translation management workflow
Next Steps
Explore specific i18n patterns:
- i18n Architecture: Structure and organization
- RTL Support: Right-to-left languages
- Date/Time Handling: Intl API
- Number Formatting: Currency, decimals
- Pluralization Rules: Complex plurals
- Lazy Loading Translations: Code splitting
- Translation Fallback: Missing keys
Remember: Global users deserve localized experiences—build for the world, not just your region.