Your app works great in English. Then someone from Tokyo tries it—dates look wrong, buttons overflow because Japanese text doesn’t fit, and your carefully designed UI breaks.
This happens because the app was built for one language and one region. To fix it, you need two things: internationalization (i18n) and localization (l10n).
What Do These Terms Mean?
Internationalization (i18n) is preparing your codebase to support multiple languages and regions. The “18” comes from the 18 letters between ‘i’ and ‘n’ in “internationalization.” It’s a one-time engineering effort where you set up the structure—extracting text from code, using flexible layouts, and handling different formats.
Localization (l10n) is adapting your app for a specific region. The “10” comes from the 10 letters between ‘l’ and ‘n’ in “localization.” This involves translating text, adjusting cultural elements, and formatting dates/currency for each target market.
The order matters: You must internationalize first, then localize. You can’t translate text that’s hardcoded in your components.
What Internationalization Looks Like
1. Extract All Text Into Separate Files
Instead of writing text directly in your code:
// Don't do this - text is stuck in English
<h1>Welcome to our app!</h1>
<p>You have 5 messages</p>
Move text to translation files and reference them by keys:
// Do this - text can be swapped for any language
<h1>{t('home.welcome')}</h1>
<p>{t('home.messages', { count: 5 })}</p>
The translation files look like this:
// locales/en.json
{
"home.welcome": "Welcome to our app!",
"home.messages": "You have messages"
}
// locales/ja.json
{
"home.welcome": "アプリへようこそ!",
"home.messages": "件のメッセージがあります"
}
2. Use Locale-Aware Formatting
Dates, numbers, and currency display differently across regions:
| Format | US | Germany | Japan |
|---|---|---|---|
| Date | 12/25/2024 | 25.12.2024 | 2024/12/25 |
| Number | 1,234.56 | 1.234,56 | 1,234.56 |
| Currency | $1,234.56 | 1.234,56 € | ¥1,235 |
Use the built-in Intl APIs instead of manual formatting:
// Dates - automatically formats based on locale
new Date().toLocaleDateString('de-DE') // "25.12.2024"
new Date().toLocaleDateString('en-US') // "12/25/2024"
// Numbers
new Intl.NumberFormat('de-DE').format(1234.56) // "1.234,56"
// Currency
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY'
}).format(1234) // "¥1,234"
3. Design Flexible Layouts
Text length varies dramatically between languages:
- German text is ~30% longer than English (“Submit” → “Einreichen”)
- Chinese text is often shorter but taller
- Arabic and Hebrew read right-to-left (RTL)
Your UI needs to handle this. Use flexible containers, avoid fixed widths for text, and test with longer strings.
4. Use UTF-8 Encoding
UTF-8 supports characters from all languages—Japanese kanji, Arabic script, emoji, everything. Set it everywhere: database, files, HTTP headers.
<meta charset="UTF-8">
What Localization Looks Like
Once your code is internationalized, localization is the ongoing work of adapting for each market:
Translation: Convert all text to the target language. Use professional translators—machine translation (Google Translate) produces awkward results that native speakers notice immediately.
Cultural adaptation: Colors, images, and symbols mean different things in different cultures. Red means danger in the West but luck in China. A thumbs-up is offensive in some Middle Eastern countries.
Format adjustment: Even after using Intl APIs, you may need to adjust how information is presented. Address formats, phone number formats, and name order (given name vs family name first) vary by region.
Legal compliance: Privacy policies, terms of service, and data handling may need adjustment for local laws (GDPR in Europe, for example).
Common Mistakes
Hardcoding strings: Every piece of user-facing text needs to be extracted. Missing even one string means a mixed-language experience.
Concatenating strings: Don’t build sentences by joining parts—word order changes between languages.
// Bad - assumes English word order
t('welcome') + ', ' + userName + '!'
// Good - lets translators control the structure
t('welcome_user', { name: userName })
Ignoring pluralization: English has simple rules (1 item, 2 items), but other languages are complex. Russian has different forms for 1, 2-4, 5-20, and 21. Use your i18n library’s pluralization features.
Using machine translation: It’s tempting for MVPs, but native speakers can tell. It damages trust.
How to Get Started
Choose a library based on your framework:
- React:
react-i18nextorreact-intl - Vue:
vue-i18n - Angular:
@angular/localize - Node.js:
i18next
Set up your file structure:
src/
locales/
en.json # English (your default)
es.json # Spanish
de.json # German
ja.json # Japanese
Use a consistent naming convention for keys:
{
"nav.home": "Home",
"nav.settings": "Settings",
"auth.login": "Log in",
"auth.logout": "Log out",
"errors.not_found": "Page not found"
}
Test with pseudo-localization: Most i18n libraries can generate fake translations that are longer and use special characters. This helps catch layout issues before real translation.
When to Implement
MVP stage: If you’re validating product-market fit in one region, skip it. Focus on the product first.
Growth stage: Implement i18n before you need it. Adding it later means touching every component. Add l10n as you expand to new markets.
Global launch: Both are required. Budget for professional translation—it’s not optional.
Cost reality: i18n is engineering time (one-time). l10n requires translators for each language (ongoing cost, typically $0.10-0.25 per word).
Summary
| i18n | l10n | |
|---|---|---|
| What | Prepare code for multiple languages | Adapt for a specific region |
| When | Once, during development | For each new market |
| Who | Developers | Translators + developers |
| Cost | Engineering time | Translation fees |
The key insight: Do i18n early, even if you only support English today. It’s much harder to retrofit later. Then add l10n market by market as you grow.
Your app can reach users worldwide. It just needs to speak their language.