Internationalization and Localization
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.
The basics
Strings as keys
Don't hard-code text:
```javascript
// 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.
Translation tables
Per-language JSON files:
```json
// 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}`).
Formatting
Numbers, dates, and currencies vary by locale:
```javascript
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.
Pluralization
Different languages have different plural rules:
- English: 1 vs many (singular/plural)
- Russian: 1, 2-4, 5+ (three forms)
- Arabic: zero, one, two, few, many, other (six forms)
Your translation library should handle this:
```javascript
t('items.count', { count: 1 }); // "1 item"
t('items.count', { count: 5 }); // "5 items"
```
The library picks the right plural form per language.
Right-to-left (RTL) languages
Arabic, Hebrew, Persian, Urdu read right-to-left. Layouts must mirror.
CSS approach:
```css
[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.
Libraries
React
- **react-intl** (FormatJS): comprehensive
- **i18next**: framework-agnostic; widely used
- **lingui**: smaller; modern
Vue
- **vue-i18n**: official solution
Vanilla / framework-agnostic
- **i18next**
- **MessageFormat**
Server-side i18n
Same libraries usually work server-side; some node-specific patterns exist.
Translation workflow
Source language
Pick one language as canonical (usually English). Translations derive from it.
Translation management systems
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.
Pseudo-localization
Run your app with strings transformed (every "a" becomes "ä", strings extended by 30%). Catches:
- Hard-coded text
- Layouts that don't accommodate longer text
- Encoding issues
Cheap technique; finds many bugs before real translation begins.
Common patterns
Locale detection
```javascript
const locale = navigator.language || 'en-US';
```
Browser tells you. User can override in app settings.
Locale stored per user
For logged-in users, save locale preference. For guests, default from browser.
Currency
Different from locale. A user might prefer English but use Euros. Store separately.
Time zones
Even more independent of locale. Store user's time zone explicitly.
Translation quality
Machine translation has improved but still produces awkward results:
- Acceptable for less-prominent text
- Not acceptable for marketing copy, errors, key UX
- Native-speaker review for important strings
For important translations, hire human translators. Machine is a starting draft, not a final answer.
Pitfalls
Concatenated strings
```javascript
// 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.
Embedded HTML in translations
```javascript
t('terms', { link: <a>terms</a> })
```
Some libraries support this; some require workarounds. Check yours.
Right-to-left bidirectional text
Hebrew with embedded English numbers, or vice versa. Browsers handle most cases; specific complex layouts may need explicit Unicode bidi marks.
Plural expressions
```javascript
// Bad
count === 1 ? "1 item" : `${count} items`
// Good
t('items.count', { count })
```
Hard-coded plurals don't work for languages with multi-form plurals.
Date formats
```javascript
// Bad
date.toString() // browser-specific format
// Good
new Intl.DateTimeFormat(locale, options).format(date)
```
Common failure patterns
- **Hard-coded strings.** Can't translate without code changes.
- **No pseudo-localization testing.** Bugs found in real translation phase.
- **Concatenated strings.** Untranslatable in some languages.
- **Wrong plural handling.** Looks fine in English; broken elsewhere.
- **No RTL testing.** Layout broken for Arabic users.
- **Locale = country.** It's not; locale is language + variant.
- **Time zones ignored.** Times shown in server time; users confused.
Further Reading
- [WebAccessibilityGuide](WebAccessibilityGuide) — i18n is part of accessibility
- [ResponsiveDesignPrinciples](ResponsiveDesignPrinciples) — Layouts must accommodate text length variation
- [TypeScriptFundamentals](TypeScriptFundamentals) — Type-safe translation keys
- [FrontendDevelopment Hub](FrontendDevelopmentHub) — Cluster index