Internationalization: Building Global Applications
i18n enables global products - plan early to avoid expensive refactoring.
Quick Navigation: Translation Setup • React Implementation • Interpolation & Pluralization • Locale-Aware Formatting • RTL (Right-to-Left) Support • Best Practices
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.jsonTranslation 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 })