Internationalization (i18n): designing your app so it can be localized. Localization (l10n): adapting it for a specific language and region.
i18n is structural; l10n is content. Both matter for global apps. Done well, supporting a new language is straightforward. Done poorly, every language addition is painful.
Don't hard-code text:
// Bad: text in code
<button>Click me</button>
// Good: keyed
<button>{t('button.click')}</button>
The key (button.click) maps to text in the user's language via a translation table.
Per-language JSON files:
// en.json
{
"button.click": "Click me",
"user.greeting": "Hello, {name}!"
}
// es.json
{
"button.click": "Haz clic",
"user.greeting": "Hola, {name}!"
}
The library substitutes variables ({name}).
Numbers, dates, and currencies vary by locale:
new Intl.NumberFormat('en-US').format(1234.56); // "1,234.56"
new Intl.NumberFormat('de-DE').format(1234.56); // "1.234,56"
new Intl.DateTimeFormat('en-US').format(new Date()); // "4/26/2026"
new Intl.DateTimeFormat('en-GB').format(new Date()); // "26/04/2026"
Intl is built into modern JavaScript. Use it; don't write custom formatting.
Different languages have different plural rules:
Your translation library should handle this:
t('items.count', { count: 1 }); // "1 item"
t('items.count', { count: 5 }); // "5 items"
The library picks the right plural form per language.
Arabic, Hebrew, Persian, Urdu read right-to-left. Layouts must mirror.
CSS approach:
[dir="rtl"] .sidebar {
/* Reverse the layout */
}
/* Or use logical properties */
.sidebar {
margin-inline-start: 16px; /* Works in both LTR and RTL */
}
Modern CSS logical properties (margin-inline-start, padding-block-end, etc.) handle RTL automatically.
Set <html dir="rtl"> for RTL languages; CSS adjusts.
Same libraries usually work server-side; some node-specific patterns exist.
Pick one language as canonical (usually English). Translations derive from it.
Tools like Lokalise, Phrase, Crowdin help manage translations across languages. Translators work in the tool; output goes back to your repo.
For small apps, JSON files in git work. For larger apps, a TMS reduces friction.
Run your app with strings transformed (every "a" becomes "ä", strings extended by 30%). Catches:
Cheap technique; finds many bugs before real translation begins.
const locale = navigator.language || 'en-US';
Browser tells you. User can override in app settings.
For logged-in users, save locale preference. For guests, default from browser.
Different from locale. A user might prefer English but use Euros. Store separately.
Even more independent of locale. Store user's time zone explicitly.
Machine translation has improved but still produces awkward results:
For important translations, hire human translators. Machine is a starting draft, not a final answer.
// Bad
"You have " + count + " messages"
// Good
t('messages.count', { count })
In some languages, the word order is different. The first form can't be translated correctly.
t('terms', { link: <a>terms</a> })
Some libraries support this; some require workarounds. Check yours.
Hebrew with embedded English numbers, or vice versa. Browsers handle most cases; specific complex layouts may need explicit Unicode bidi marks.
// Bad
count === 1 ? "1 item" : `${count} items`
// Good
t('items.count', { count })
Hard-coded plurals don't work for languages with multi-form plurals.
// Bad
date.toString() // browser-specific format
// Good
new Intl.DateTimeFormat(locale, options).format(date)