--- name: i18n-localization description: Internationalization and localization for global applications. Use when adding multi-language support, handling regional formats, or preparing apps for global markets. --- # Internationalization & Localization Comprehensive guide for building globally-ready applications. ## Key Concepts ### Terminology ``` i18n (Internationalization): - Engineering for multiple languages/regions - Building the infrastructure - Happens once, during development L10n (Localization): - Adapting for specific locale - Translation, formatting, content - Happens per locale, ongoing Locale: - Language + Region combination - Format: language-REGION (e.g., en-US, pt-BR) - Determines formatting rules ``` ### What Needs Localization? | Category | Examples | | ------------- | ---------------------------- | | **Text** | UI labels, messages, errors | | **Numbers** | 1,234.56 vs 1.234,56 | | **Dates** | MM/DD/YYYY vs DD/MM/YYYY | | **Times** | 12-hour vs 24-hour | | **Currency** | $1,234.56 vs €1.234,56 | | **Plurals** | 1 item vs 2 items vs 5 items | | **Direction** | LTR vs RTL | | **Images** | Cultural appropriateness | | **Colors** | Cultural significance | | **Names** | Order, formality | --- ## Text & Messages ### Externalize All Strings ```typescript // DON'T: Hardcoded strings const message = "Welcome back, " + username; // DO: Externalized, translatable const message = t('welcome_back', { name: username }); // Translation file (en.json) { "welcome_back": "Welcome back, {{name}}" } // Translation file (es.json) { "welcome_back": "Bienvenido de nuevo, {{name}}" } ``` ### Message Format ```typescript // ICU Message Format (recommended) // Handles plurals, select, dates, numbers // Simple "greeting": "Hello, {name}!" // Plural "items_count": "{count, plural, =0 {No items} one {# item} other {# items}}" // Select (gender, etc.) "notification": "{gender, select, male {He} female {She} other {They}} liked your post" // Nested "cart": "{itemCount, plural, =0 {Your cart is empty} one {You have # item in your cart} other {You have # items in your cart} }" ``` ### Pluralization ``` Languages have different plural rules: English: 1 (one), 2+ (other) French: 0-1 (one), 2+ (other) Russian: Complex rules for 1, 2-4, 5-20, 21, etc. Arabic: 6 plural forms! Chinese: No plural forms Always use library pluralization, never DIY: - react-intl / formatjs - i18next - Intl.PluralRules API ``` --- ## Date & Time ### JavaScript Intl API ```typescript // Date formatting const date = new Date("2024-03-15"); new Intl.DateTimeFormat("en-US").format(date); // "3/15/2024" new Intl.DateTimeFormat("de-DE").format(date); // "15.3.2024" new Intl.DateTimeFormat("ja-JP").format(date); // "2024/3/15" // With options new Intl.DateTimeFormat("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", }).format(date); // "Friday, March 15, 2024" ``` ### Time Zones ```typescript // Always store in UTC const utcDate = new Date().toISOString(); // "2024-03-15T14:30:00.000Z" // Display in user's timezone new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York", dateStyle: "full", timeStyle: "long", }).format(new Date(utcDate)); // "Friday, March 15, 2024 at 10:30:00 AM EDT" // Get user's timezone const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; ``` ### Relative Time ```typescript const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); rtf.format(-1, "day"); // "yesterday" rtf.format(1, "day"); // "tomorrow" rtf.format(-3, "hour"); // "3 hours ago" rtf.format(2, "week"); // "in 2 weeks" // For automatic unit selection, use a library like date-fns import { formatDistanceToNow } from "date-fns"; formatDistanceToNow(date, { addSuffix: true }); // "3 days ago" ``` --- ## Numbers & Currency ### Number Formatting ```typescript 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" new Intl.NumberFormat("fr-FR").format(number); // "1 234 567,89" // Percentages new Intl.NumberFormat("en-US", { style: "percent", minimumFractionDigits: 1, }).format(0.256); // "25.6%" ``` ### Currency Formatting ```typescript const amount = 1234.56; new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(amount); // "$1,234.56" new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR", }).format(amount); // "1.234,56 €" new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY", }).format(amount); // "¥1,235" (no decimal for JPY) ``` ### Currency Best Practices ```typescript // Store amount + currency code interface Money { amount: number; // In smallest unit (cents) currency: string; // ISO 4217 code (USD, EUR, etc.) } // Format for display function formatMoney(money: Money, locale: string): string { return new Intl.NumberFormat(locale, { style: "currency", currency: money.currency, }).format(money.amount / 100); // Convert cents to units } // Handle exchange rates on backend // Never convert currencies client-side ``` --- ## RTL (Right-to-Left) Support ### RTL Languages ``` RTL Languages: - Arabic (ar) - Hebrew (he) - Persian/Farsi (fa) - Urdu (ur) Layout considerations: ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ Logo Menu Settings │ │ Settings Menu Logo │ │ ← Back │ │ Back → │ │ [Icon] Label │ │ Label [Icon] │ │ Next → │ │ ← Next │ └─────────────────────────────┘ └─────────────────────────────┘ LTR Layout RTL Layout ``` ### CSS for RTL ```css /* Use logical properties */ /* DON'T: Physical properties */ .element { margin-left: 10px; padding-right: 20px; text-align: left; float: left; } /* DO: Logical properties */ .element { margin-inline-start: 10px; padding-inline-end: 20px; text-align: start; float: inline-start; } /* Set direction based on locale */ html[dir="rtl"] { direction: rtl; } /* Or use :dir() pseudo-class */ .icon:dir(rtl) { transform: scaleX(-1); /* Mirror icons */ } /* Flexbox auto-reverses in RTL */ .container { display: flex; /* No need to change for RTL */ } ``` ### HTML Direction ```html
مرحبا بالعالم

The word مرحبا means "hello".

``` --- ## Implementation (React) ### react-intl Setup ```typescript // src/i18n/index.ts import { createIntl, createIntlCache } from '@formatjs/intl'; const cache = createIntlCache(); const messages = { en: () => import('./locales/en.json'), es: () => import('./locales/es.json'), fr: () => import('./locales/fr.json'), }; export async function loadMessages(locale: string) { const loader = messages[locale] || messages.en; return (await loader()).default; } // App.tsx import { IntlProvider } from 'react-intl'; function App() { const [locale, setLocale] = useState('en'); const [messages, setMessages] = useState({}); useEffect(() => { loadMessages(locale).then(setMessages); }, [locale]); return ( ); } ``` ### Using Translations ```tsx import { FormattedMessage, useIntl } from "react-intl"; function ProductCard({ product }) { const intl = useIntl(); return (
{/* Component-based */}

{/* Hook-based (for attributes, etc.) */} {intl.formatMessage({ {/* Numbers */}

{/* Dates */}

{/* Plurals */}

); } ``` ### Translation Files ```json // locales/en.json { "product.title": "{name}", "product.image_alt": "Photo of {name}", "product.reviews": "{count, plural, =0 {No reviews} one {# review} other {# reviews}}", "cart.empty": "Your cart is empty", "cart.checkout": "Proceed to checkout" } // locales/es.json { "product.title": "{name}", "product.image_alt": "Foto de {name}", "product.reviews": "{count, plural, =0 {Sin reseñas} one {# reseña} other {# reseñas}}", "cart.empty": "Tu carrito está vacío", "cart.checkout": "Proceder al pago" } ``` --- ## Implementation (Node.js) ### i18next Setup ```typescript import i18next from "i18next"; import Backend from "i18next-fs-backend"; await i18next.use(Backend).init({ lng: "en", fallbackLng: "en", supportedLngs: ["en", "es", "fr", "de"], backend: { loadPath: "./locales/{{lng}}/{{ns}}.json", }, interpolation: { escapeValue: false, }, }); // Usage const t = i18next.t; t("welcome", { name: "John" }); // "Welcome, John!" // Change language await i18next.changeLanguage("es"); ``` ### Express Middleware ```typescript import { Request, Response, NextFunction } from "express"; function detectLocale(req: Request, res: Response, next: NextFunction) { // Priority: Query param > Cookie > Accept-Language header > Default const locale = req.query.locale || req.cookies.locale || req.acceptsLanguages(["en", "es", "fr"]) || "en"; req.locale = locale; req.t = i18next.getFixedT(locale); next(); } app.use(detectLocale); app.get("/api/greeting", (req, res) => { res.json({ message: req.t("greeting", { name: req.user.name }), }); }); ``` --- ## Translation Workflow ### File Structure ``` locales/ ├── en/ │ ├── common.json # Shared strings │ ├── auth.json # Auth-related │ ├── products.json # Product-related │ └── errors.json # Error messages ├── es/ │ └── ... ├── fr/ │ └── ... └── _source/ └── en.json # Source of truth ``` ### Key Naming Convention ```json { // Feature.element.description "auth.login.button": "Sign In", "auth.login.error.invalid_credentials": "Invalid email or password", // Or flat with prefixes "btn_login": "Sign In", "err_invalid_credentials": "Invalid email or password" // Consistent, descriptive, unique } ``` ### Translation Management ``` TOOLS: - Lokalise, Crowdin, Phrase (SaaS platforms) - Weblate (open source) - POEditor, Transifex WORKFLOW: 1. Extract strings from code 2. Upload to translation platform 3. Translators work in context 4. Review translations 5. Download and integrate 6. Test in app 7. Repeat for changes AUTOMATION: - CI/CD integration - Automatic string extraction - Translation memory - Machine translation + human review ``` --- ## Testing ### Pseudo-Localization ```typescript // Transforms: "Hello" → "[Ħëľľö!!!]" // Helps identify: // - Hardcoded strings // - Text truncation issues // - Character encoding problems // - Layout issues with longer text function pseudoLocalize(text: string): string { const charMap: Record = { a: "ä", b: "β", c: "ç", d: "δ", e: "ë", // ... etc }; const transformed = text .split("") .map((c) => charMap[c.toLowerCase()] || c) .join(""); // Add padding (most translations are 30% longer) const padding = "!".repeat(Math.ceil(text.length * 0.3)); return `[${transformed}${padding}]`; } ``` ### Locale Testing Checklist ``` FOR EACH LOCALE: □ All strings translated □ No hardcoded text visible □ Dates format correctly □ Numbers format correctly □ Currency displays properly □ Plurals work for all cases □ UI accommodates text length □ RTL layout correct (if applicable) □ Images are appropriate □ No encoding issues □ Forms validate appropriately ``` --- ## Best Practices ### DO: - Externalize ALL user-facing strings - Use ICU message format for complex strings - Support locale switching without reload - Test with pseudo-localization - Store dates in UTC, display in local time - Use native Intl APIs when possible - Plan for text expansion (30-50% longer) - Support RTL from the start ### DON'T: - Hardcode any user-facing text - Concatenate strings for messages - Assume date/number formats - Handle plurals manually - Forget about cultural context - Translate in code (use translation files) - Ignore bidirectional text issues - Skip testing with real languages --- ## Checklist ### Initial Setup - [ ] Choose i18n library - [ ] Set up translation file structure - [ ] Configure locale detection - [ ] Add locale switcher UI - [ ] Set up RTL support ### Per Feature - [ ] Extract all strings to translation files - [ ] Use message format for variables - [ ] Handle plurals correctly - [ ] Format dates/numbers/currency - [ ] Test with pseudo-localization - [ ] Review text in context ### Launch - [ ] Professional translations complete - [ ] All locales tested - [ ] RTL layouts verified - [ ] SEO for multi-language (hreflang) - [ ] Locale-specific content reviewed