Front-end Engineering Lab
PatternsInternationalization

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/31

5. 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);
// ١٠٠٫٠٠ ر.س.
LibraryBest ForSizeFeatures
next-intlNext.js appsSmallServer + Client, Type-safe
react-i18nextReact appsMediumMature, Full-featured
FormatJS (react-intl)EnterpriseMediumICU messages, Polyfills
i18nextAny frameworkSmallFramework-agnostic
LinguiPerformanceSmallCompile-time, Minimal runtime

Basic Setup (next-intl)

Installation

npm install next-intl

Configuration

// 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.json

2. 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-translations

2. Type-Safe Translations

// next-intl provides automatic type checking
const t = useTranslations('cart');
t('items', { count: 5 }); // ✅ Type-safe
t('nonexistent'); // ❌ TypeScript error

3. 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:

Remember: Global users deserve localized experiences—build for the world, not just your region.

On this page