Internationalization: Building Global Applications

Easy•

i18n enables global products - plan early to avoid expensive refactoring.

Quick Decision Guide

i18n Essentials:

1. Translation files per language (en.json, fr.json) 2. Library (react-i18next, next-intl) for dynamic switching 3. Formatters for dates, numbers, currencies 4. RTL support for Arabic/Hebrew with CSS

Result: Global reach, better UX for non-English users, increased market.

Translation Setup

File Structure

/locales
  /en
    common.json
    dashboard.json
  /fr
    common.json
    dashboard.json
  /es
    common.json
    dashboard.json

Translation Files

// locales/en/common.json
{
  "welcome": "Welcome",
  "login": "Log in",
  "logout": "Log out",
  "settings": "Settings",
  "profile": "Profile"
}

// locales/fr/common.json
{
  "welcome": "Bienvenue",
  "login": "Se connecter",
  "logout": "Se déconnecter",
  "settings": "Paramètres",
  "profile": "Profil"
}

Nested Keys

{
  "auth": {
    "login": {
      "title": "Log in to your account",
      "email": "Email address",
      "password": "Password",
      "submit": "Sign in",
      "forgotPassword": "Forgot password?"
    }
  }
}

Usage: t('auth.login.title')

React Implementation

react-i18next Setup

// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import en from './locales/en/common.json';
import fr from './locales/fr/common.json';

i18n
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: en },
      fr: { translation: fr },
    },
    lng: 'en', // default language
    fallbackLng: 'en',
    interpolation: {
      escapeValue: false, // React already escapes
    },
  });

export default i18n;

Usage in Components

import { useTranslation } from 'react-i18next';

function LoginForm() {
  const { t } = useTranslation();
  
  return (
    <form>
      <h1>{t('auth.login.title')}</h1>
      <input placeholder={t('auth.login.email')} />
      <input type="password" placeholder={t('auth.login.password')} />
      <button>{t('auth.login.submit')}</button>
    </form>
  );
}

Language Switcher

function LanguageSwitcher() {
  const { i18n } = useTranslation();
  
  return (
    <select 
      value={i18n.language}
      onChange={(e) => i18n.changeLanguage(e.target.value)}
    >
      <option value="en">English</option>
      <option value="fr">Français</option>
      <option value="es">Español</option>
    </select>
  );
}

Interpolation & Pluralization

Variable Interpolation

{
  "greeting": "Hello, {{name}}!",
  "itemsInCart": "You have {{count}} items in your cart"
}
t('greeting', { name: 'John' })
// Output: "Hello, John!"

t('itemsInCart', { count: 3 })
// Output: "You have 3 items in your cart"

Pluralization

{
  "item_one": "{{count}} item",
  "item_other": "{{count}} items"
}
t('item', { count: 1 })  // "1 item"
t('item', { count: 5 })  // "5 items"

Context

{
  "friend_male": "He is your friend",
  "friend_female": "She is your friend"
}
t('friend', { context: 'male' })   // "He is your friend"
t('friend', { context: 'female' }) // "She is your friend"

Locale-Aware Formatting

Dates

// Using Intl.DateTimeFormat
const date = new Date('2024-01-15');

new Intl.DateTimeFormat('en-US').format(date);
// "1/15/2024"

new Intl.DateTimeFormat('fr-FR').format(date);
// "15/01/2024"

new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
}).format(date);
// "January 15, 2024"

Numbers

const number = 1234567.89;

new Intl.NumberFormat('en-US').format(number);
// "1,234,567.89"

new Intl.NumberFormat('de-DE').format(number);
// "1.234.567,89"

Currency

const price = 1234.56;

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).format(price);
// "$1,234.56"

new Intl.NumberFormat('fr-FR', {
  style: 'currency',
  currency: 'EUR'
}).format(price);
// "1 234,56 €"

Relative Time

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

rtf.format(-1, 'day');  // "yesterday"
rtf.format(2, 'day');   // "in 2 days"
rtf.format(-3, 'month'); // "3 months ago"

RTL (Right-to-Left) Support

CSS for RTL

/* Automatic flipping with logical properties */
.container {
  /* Instead of margin-left */
  margin-inline-start: 1rem;
  
  /* Instead of padding-right */
  padding-inline-end: 1rem;
  
  /* Instead of text-align: left */
  text-align: start;
}

HTML Direction

function App() {
  const { i18n } = useTranslation();
  const direction = i18n.dir(); // 'ltr' or 'rtl'
  
  return (
    <html lang={i18n.language} dir={direction}>
      <body>
        <App />
      </body>
    </html>
  );
}

RTL-Aware Tailwind

<div className="ltr:ml-4 rtl:mr-4">
  Content
</div>

Direction-Specific Styles

[dir="ltr"] .nav {
  border-left: 1px solid #ccc;
}

[dir="rtl"] .nav {
  border-right: 1px solid #ccc;
}

Best Practices

Do's

âś… Externalize all text - No hardcoded strings

// ❌ Bad
<button>Submit</button>

// âś… Good
<button>{t('form.submit')}</button>

âś… Use namespaces - Organize translations

t('dashboard:widgets.sales')
t('auth:login.title')

âś… Provide context - Help translators

{
  "save": "Save",
  "_comment": "Button text for saving form data"
}

âś… Test with real translations - Not just pseudo-localization

Don'ts

❌ Don't concatenate strings

// ❌ Bad: Word order differs in languages
{t('you_have')} {count} {t('items')}

// âś… Good: Single translatable string
{t('items_in_cart', { count })}

❌ Don't assume text length

/* ❌ Bad: German words are longer */
.button {
  width: 100px; /* Might truncate */
}

/* âś… Good: Flexible */
.button {
  min-width: 100px;
  padding: 0.5rem 1rem;
}

❌ Don't forget plurals

// ❌ Bad
`${count} item(s)`

// âś… Good
t('item', { count })