Internationalization (i18n) & Localization
Overview
Comprehensive guide to implementing internationalization and localization in applications. Covers message translation, pluralization, date/time/number formatting, RTL languages, and integration with popular i18n libraries.
When to Use
- Building multi-language applications
- Supporting international users
- Implementing language switching
- Formatting dates, times, and numbers for different locales
- Supporting RTL (right-to-left) languages
- Extracting and managing translation strings
- Implementing pluralization rules
- Setting up translation workflows
Instructions
1. i18next (JavaScript/TypeScript)
Basic Setup
// i18n.ts
import i18next from "i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
await i18next
.use(Backend)
.use(LanguageDetector)
.init({
fallbackLng: "en",
debug: process.env.NODE_ENV === "development",
interpolation: {
escapeValue: false, // React already escapes
},
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
detection: {
order: ["querystring", "cookie", "localStorage", "navigator"],
caches: ["localStorage", "cookie"],
},
});
export default i18next;
Translation Files
// locales/en/translation.json
{
"welcome": "Welcome to our app",
"greeting": "Hello, {{name}}!",
"itemCount": "You have {{count}} item",
"itemCount_plural": "You have {{count}} items",
"user": {
"profile": "User Profile",
"settings": "Settings",
"logout": "Log out"
},
"validation": {
"required": "This field is required",
"email": "Please enter a valid email",
"minLength": "Must be at least {{min}} characters"
}
}
// locales/es/translation.json
{
"welcome": "Bienvenido a nuestra aplicación",
"greeting": "¡Hola, {{name}}!",
"itemCount": "Tienes {{count}} artículo",
"itemCount_plural": "Tienes {{count}} artículos",
"user": {
"profile": "Perfil de Usuario",
"settings": "Configuración",
"logout": "Cerrar sesión"
},
"validation": {
"required": "Este campo es obligatorio",
"email": "Por favor ingrese un correo válido",
"minLength": "Debe tener al menos {{min}} caracteres"
}
}
// locales/fr/translation.json
{
"welcome": "Bienvenue dans notre application",
"greeting": "Bonjour, {{name}} !",
"itemCount": "Vous avez {{count}} article",
"itemCount_plural": "Vous avez {{count}} articles",
"user": {
"profile": "Profil utilisateur",
"settings": "Paramètres",
"logout": "Se déconnecter"
}
}
React Integration
// App.tsx
import { useTranslation } from 'react-i18next';
import './i18n';
export function App() {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('greeting', { name: 'John' })}</p>
<p>{t('itemCount', { count: 5 })}</p>
{/* Language switcher */}
<select
value={i18n.language}
onChange={(e) => changeLanguage(e.target.value)}
>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
</div>
);
}
// Component with namespace
export function UserProfile() {
const { t } = useTranslation('user');
return (
<div>
<h2>{t('profile')}</h2>
<button>{t('logout')}</button>
</div>
);
}
Node.js/Express Backend
// i18n-middleware.ts
import i18next from "i18next";
import Backend from "i18next-fs-backend";
import middleware from "i18next-http-middleware";
i18next
.use(Backend)
.use(middleware.LanguageDetector)
.init({
fallbackLng: "en",
preload: ["en", "es", "fr"],
backend: {
loadPath: "./locales/{{lng}}/{{ns}}.json",
},
});
export const i18nMiddleware = middleware.handle(i18next);
// app.ts
import express from "express";
import { i18nMiddleware } from "./i18n-middleware";
const app = express();
app.use(i18nMiddleware);
app.get("/api/welcome", (req, res) => {
res.json({
message: req.t("welcome"),
greeting: req.t("greeting", { name: "User" }),
});
});
2. React-Intl (Format.js)
// IntlProvider setup
import { IntlProvider } from 'react-intl';
import messages_en from './translations/en.json';
import messages_es from './translations/es.json';
const messages = {
en: messages_en,
es: messages_es
};
export function App() {
const [locale, setLocale] = useState('en');
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<YourApp />
</IntlProvider>
);
}
// Using translations
import { FormattedMessage, useIntl } from 'react-intl';
export function Welcome() {
const intl = useIntl();
return (
<div>
{/* Basic translation */}
<h1>
<FormattedMessage id="welcome" defaultMessage="Welcome" />
</h1>
{/* With variables */}
<p>
<FormattedMessage
id="greeting"
defaultMessage="Hello, {name}!"
values={{ name: 'John' }}
/>
</p>
{/* Pluralization */}
<p>
<FormattedMessage
id="itemCount"
defaultMessage="{count, plural, =0 {No items} one {# item} other {# items}}"
values={{ count: 5 }}
/>
</p>
{/* In code */}
<button title={intl.formatMessage({ id: 'submit' })}>
{intl.formatMessage({ id: 'submit' })}
</button>
</div>
);
}
3. Python i18n (gettext)
# i18n.py
import gettext
import os
class I18n:
def __init__(self, locale='en', domain='messages'):
self.locale = locale
self.domain = domain
self._translator = None
self._load_translations()
def _load_translations(self):
locale_dir = os.path.join(os.path.dirname(__file__), 'locales')
try:
self._translator = gettext.translation(
self.domain,
localedir=locale_dir,
languages=[self.locale]
)
except FileNotFoundError:
# Fall back to NullTranslations (no translation)
self._translator = gettext.NullTranslations()
def t(self, message, **kwargs):
"""Translate message with optional variable substitution"""
translated = self._translator.gettext(message)
if kwargs:
return translated.format(**kwargs)
return translated
def tn(self, singular, plural, n, **kwargs):
"""Translate with pluralization"""
translated = self._translator.ngettext(singular, plural, n)
if kwargs:
return translated.format(n=n, **kwargs)
return translated
# Usage
i18n = I18n(locale='es')
print(i18n.t("Welcome to our app"))
print(i18n.t("Hello, {name}!", name="Juan"))
print(i18n.tn("You have {n} item", "You have {n} items", 5))
# Extracting messages for translation
# Install: pip install Babel
# babel.cfg
[python: **.py]
# Extract messages
# pybabel extract -F babel.cfg -o locales/messages.pot .
# Initialize new language
# pybabel init -i locales/messages.pot -d locales -l es
# Compile translations
# pybabel compile -d locales
4. Date and Time Formatting
JavaScript (Intl API)
// date-formatter.ts
export class DateFormatter {
constructor(private locale: string) {}
// Format date
formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string {
return new Intl.DateTimeFormat(this.locale, options).format(date);
}
// Predefined formats
short(date: Date): string {
return this.formatDate(date, {
year: "numeric",
month: "short",
day: "numeric",
});
}
long(date: Date): string {
return this.formatDate(date, {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
});
}
time(date: Date): string {
return this.formatDate(date, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
relative(date: Date): string {
const rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: "auto" });
const diff = date.getTime() - Date.now();
const days = Math.round(diff / (1000 * 60 * 60 * 24));
if (Math.abs(days) < 1) {
const hours = Math.round(diff / (1000 * 60 * 60));
return rtf.format(hours, "hour");
}
return rtf.format(days, "day");
}
}
// Usage
const enFormatter = new DateFormatter("en-US");
const esFormatter = new DateFormatter("es-ES");
const jaFormatter = new DateFormatter("ja-JP");
const date = new Date("2024-01-15");
console.log(enFormatter.short(date)); // Jan 15, 2024
console.log(esFormatter.short(date)); // 15 ene 2024
console.log(jaFormatter.short(date)); // 2024年1月15日
console.log(enFormatter.relative(new Date(Date.now() - 86400000))); // yesterday
React-Intl Date Formatting
import { FormattedDate, FormattedTime, FormattedRelativeTime } from 'react-intl';
export function DateDisplay() {
const date = new Date();
return (
<div>
{/* Date */}
<FormattedDate
value={date}
year="numeric"
month="long"
day="numeric"
/>
{/* Time */}
<FormattedTime value={date} />
{/* Relative time */}
<FormattedRelativeTime
value={-1}
unit="day"
updateIntervalInSeconds={60}
/>
</div>
);
}
5. Number and Currency Formatting
// number-formatter.ts
export class NumberFormatter {
constructor(private locale: string) {}
// Format number
formatNumber(value: number, options?: Intl.NumberFormatOptions): string {
return new Intl.NumberFormat(this.locale, options).format(value);
}
// Currency
currency(value: number, currency: string): string {
return this.formatNumber(value, {
style: "currency",
currency,
});
}
// Percentage
percent(value: number): string {
return this.formatNumber(value, {
style: "percent",
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
}
// Decimal
decimal(value: number, decimals: number = 2): string {
return this.formatNumber(value, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
}
// Compact notation (1.2K, 1.5M)
compact(value: number): string {
return this.formatNumber(value, {
notation: "compact",
compactDisplay: "short",
});
}
}
// Usage
const enFormatter = new NumberFormatter("en-US");
const deFormatter = new NumberFormatter("de-DE");
const jaFormatter = new NumberFormatter("ja-JP");
console.log(enFormatter.currency(1234.56, "USD")); // $1,234.56
console.log(deFormatter.currency(1234.56, "EUR")); // 1.234,56 €
console.log(jaFormatter.currency(1234.56, "JPY")); // ¥1,235
console.log(enFormatter.percent(0.1234)); // 12.34%
console.log(enFormatter.compact(1234567)); // 1.2M
6. Pluralization Rules
// pluralization.ts
export class PluralRules {
constructor(private locale: string) {}
// Get plural category
select(count: number): Intl.LDMLPluralRule {
const pr = new Intl.PluralRules(this.locale);
return pr.select(count);
}
// Format with pluralization
format(count: number, forms: Record<Intl.LDMLPluralRule, string>): string {
const rule = this.select(count);
return forms[rule] || forms.other;
}
}
// Usage
const enRules = new PluralRules("en");
console.log(
enRules.format(0, {
zero: "No items",
one: "One item",
other: "{{count}} items",
}),
);
console.log(
enRules.format(1, {
one: "One item",
other: "{{count}} items",
}),
);
// Different languages have different plural rules
const arRules = new PluralRules("ar"); // Arabic has 6 plural forms
const plRules = new PluralRules("pl"); // Polish has complex plural rules
ICU Message Format
// Using intl-messageformat
import IntlMessageFormat from "intl-messageformat";
const message = new IntlMessageFormat(
"{count, plural, =0 {No items} one {# item} other {# items}}",
"en",
);
console.log(message.format({ count: 0 })); // No items
console.log(message.format({ count: 1 })); // 1 item
console.log(message.format({ count: 5 })); // 5 items
// With gender
const genderMessage = new IntlMessageFormat(
"{gender, select, male {He} female {She} other {They}} bought {count, plural, one {# item} other {# items}}",
"en",
);
console.log(genderMessage.format({ gender: "female", count: 2 }));
// She bought 2 items
7. RTL (Right-to-Left) Language Support
// rtl-utils.ts
const RTL_LANGUAGES = ["ar", "he", "fa", "ur"];
export function isRTL(locale: string): boolean {
const lang = locale.split("-")[0];
return RTL_LANGUAGES.includes(lang);
}
export function getDirection(locale: string): "ltr" | "rtl" {
return isRTL(locale) ? "rtl" : "ltr";
}
/* styles/rtl.css */
:root {
--text-align-start: left;
--text-align-end: right;
--margin-start: margin-left;
--margin-end: margin-right;
--padding-start: padding-left;
--padding-end: padding-right;
}
[dir="rtl"] {
--text-align-start: right;
--text-align-end: left;
--margin-start: margin-right;
--margin-end: margin-left;
--padding-start: padding-right;
--padding-end: padding-left;
}
.container {
text-align: var(--text-align-start);
margin-left: var(--margin-start);
padding-right: var(--padding-end);
}
/* Or use logical properties (modern approach) */
.modern-container {
text-align: start;
margin-inline-start: 1rem;
padding-inline-end: 2rem;
}
// RTL React component
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { isRTL, getDirection } from './rtl-utils';
export function App() {
const { i18n } = useTranslation();
useEffect(() => {
const direction = getDirection(i18n.language);
document.documentElement.setAttribute('dir', direction);
document.documentElement.setAttribute('lang', i18n.language);
}, [i18n.language]);
return (
<div className="app">
{/* Your app content */}
</div>
);
}
8. Translation Management
Message Extraction
// extract-messages.ts
import { sync as globSync } from "glob";
import fs from "fs";
const TRANSLATION_PATTERN = /t\(['"]([^'"]+)['"]\)/g;
export function extractMessages(pattern: string): Set<string> {
const messages = new Set<string>();
const files = globSync(pattern);
for (const file of files) {
const content = fs.readFileSync(file, "utf8");
let match;
while ((match = TRANSLATION_PATTERN.exec(content)) !== null) {
messages.add(match[1]);
}
}
return messages;
}
// Generate translation template
export function generateTemplate(messages: Set<string>): object {
const template: Record<string, string> = {};
for (const message of messages) {
template[message] = message; // Default to English
}
return template;
}
// Usage
const messages = extractMessages("src/**/*.{ts,tsx}");
const template = generateTemplate(messages);
fs.writeFileSync(
"locales/en/translation.json",
JSON.stringify(template, null, 2),
);
Translation Status
// check-translations.ts
export function checkTranslationStatus(
baseLocale: object,
targetLocale: object,
): {
missing: string[];
extra: string[];
coverage: number;
} {
const baseKeys = new Set(Object.keys(baseLocale));
const targetKeys = new Set(Object.keys(targetLocale));
const missing = [...baseKeys].filter((key) => !targetKeys.has(key));
const extra = [...targetKeys].filter((key) => !baseKeys.has(key));
const coverage = (targetKeys.size / baseKeys.size) * 100;
return { missing, extra, coverage };
}
// Usage
const enMessages = require("./locales/en/translation.json");
const esMessages = require("./locales/es/translation.json");
const status = checkTranslationStatus(enMessages, esMessages);
console.log(`Spanish translation coverage: ${status.coverage.toFixed(2)}%`);
console.log(`Missing keys: ${status.missing.join(", ")}`);
9. Locale Detection
// locale-detector.ts
export class LocaleDetector {
// Detect from browser
static fromBrowser(): string {
return navigator.language || navigator.languages[0] || "en";
}
// Detect from URL
static fromURL(): string | null {
const params = new URLSearchParams(window.location.search);
return params.get("lang") || params.get("locale");
}
// Detect from cookie
static fromCookie(name: string = "locale"): string | null {
const match = document.cookie.match(new RegExp(`${name}=([^;]+)`));
return match ? match[1] : null;
}
// Detect from localStorage
static fromStorage(key: string = "locale"): string | null {
return localStorage.getItem(key);
}
// Detect with priority
static detect(defaultLocale: string = "en"): string {
return (
this.fromURL() ||
this.fromStorage() ||
this.fromCookie() ||
this.fromBrowser() ||
defaultLocale
);
}
// Save locale
static save(locale: string): void {
localStorage.setItem("locale", locale);
document.cookie = `locale=${locale}; path=/; max-age=31536000`;
}
}
10. Server-Side i18n
// Next.js i18n configuration
// next.config.js
module.exports = {
i18n: {
locales: ['en', 'es', 'fr', 'de', 'ja'],
defaultLocale: 'en',
localeDetection: true
}
};
// pages/index.tsx
import { GetStaticProps } from 'next';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
export default function Home() {
const { t } = useTranslation('common');
return (
<div>
<h1>{t('welcome')}</h1>
</div>
);
}
export const getStaticProps: GetStaticProps = async ({ locale }) => {
return {
props: {
...(await serverSideTranslations(locale ?? 'en', ['common']))
}
};
};
Best Practices
✅ DO
- Extract all user-facing strings to translation files
- Use ICU message format for complex messages
- Support pluralization correctly for each language
- Use locale-aware date/time/number formatting
- Implement RTL support for Arabic, Hebrew, etc.
- Provide fallback language (usually English)
- Use namespaces to organize translations
- Test with pseudo-localization (ääçćëńţś)
- Store locale preference (cookie, localStorage)
- Use professional translators for production
- Implement translation management workflow
- Support dynamic locale switching
- Use translation memory tools
❌ DON'T
- Hardcode user-facing strings in code
- Concatenate translated strings
- Assume English grammar rules apply to all languages
- Use generic plural forms (one/many) for all languages
- Forget about text expansion (German is ~30% longer)
- Store dates/times in locale-specific formats
- Use flags to represent languages (flag ≠ language)
- Translate technical terms without context
- Mix translation keys with UI strings
- Forget to translate alt text, titles, placeholders
- Assume left-to-right layout
Common Patterns
Pattern 1: Translation Hook
export function useLocale() {
const { i18n } = useTranslation();
return {
locale: i18n.language,
changeLocale: (lng: string) => i18n.changeLanguage(lng),
t: i18n.t,
formatDate: (date: Date) => new DateFormatter(i18n.language).short(date),
formatNumber: (num: number) =>
new NumberFormatter(i18n.language).formatNumber(num),
formatCurrency: (amount: number, currency: string) =>
new NumberFormatter(i18n.language).currency(amount, currency),
};
}
Pattern 2: Language Switcher Component
export function LanguageSwitcher() {
const { locale, changeLocale } = useLocale();
const languages = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
{ code: 'fr', name: 'French', nativeName: 'Français' },
{ code: 'de', name: 'German', nativeName: 'Deutsch' }
];
return (
<select value={locale} onChange={(e) => changeLocale(e.target.value)}>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</select>
);
}
Tools & Resources
- i18next: Comprehensive i18n framework
- react-intl (Format.js): React i18n library
- LinguiJS: Developer-friendly i18n
- vue-i18n: Vue.js i18n plugin
- Crowdin: Translation management platform
- Lokalise: Localization management
- Phrase: Localization platform
- POEditor: Translation management
- BabelEdit: Translation editor
- Pseudolocalization: Testing tool