i18n Best Practices: 15 Rules for Internationalization in 2024
Master internationalization with these 15 essential best practices. From code architecture to translation management, build apps that scale globally.
Introduction
After helping thousands of teams internationalize their apps, we've identified the practices that separate smooth, scalable i18n implementations from painful, buggy ones.
Here are 15 rules to follow for successful internationalization.
Code Architecture
1. Extract All User-Facing Strings from Day One
Don't wait until "we need to translate." By then, you'll have thousands of hardcoded strings.
// Bad - hardcoded string
<button>Submit</button>
// Good - extracted from start
<button>{t('form.submit')}</button>Tip: Use IntlPull's CLI scanner to find missed strings:
npx intlpull scan --dry-run2. Never Concatenate Strings
Different languages have different word orders. Concatenation breaks translations.
// Bad - breaks in German, Japanese, Arabic...
const message = "Hello " + name + ", you have " + count + " messages";
// Good - uses interpolation
const message = t('greeting', { name, count });
// en: "Hello {name}, you have {count} messages"
// de: "Hallo {name}, Sie haben {count} Nachrichten"
// ja: "{name}さん、{count}件のメッセージがあります"3. Use ICU Message Format for Complex Strings
ICU handles plurals, gender, and selection elegantly:
{count, plural,
=0 {No messages}
one {# message}
other {# messages}
}
{gender, select,
male {He liked your post}
female {She liked your post}
other {They liked your post}
}4. Keep Translation Keys Semantic
Keys should describe content, not location:
// Bad - tied to UI location
"homepage_header_title"
"sidebar_menu_item_3"
// Good - describes content
"welcome.title"
"navigation.settings"
"error.network_unavailable"5. Organize Keys by Feature, Not File
Structure translations by feature domain:
/locales/en.json
{
"auth": {
"login": { ... },
"signup": { ... }
},
"dashboard": {
"overview": { ... },
"settings": { ... }
},
"checkout": {
"cart": { ... },
"payment": { ... }
}
}Formatting
6. Use Native Formatting APIs
Never hardcode number, date, or currency formats:
// Bad - US-only format
const date = `${month}/${day}/${year}`;
const price = `$${amount}`;
// Good - locale-aware
const date = new Intl.DateTimeFormat(locale).format(dateObj);
const price = new Intl.NumberFormat(locale, {
style: 'currency',
currency: userCurrency
}).format(amount);Locale differences:
| US | Germany | Japan | |
|---|---|---|---|
| Date | 12/31/2024 | 31.12.2024 | 2024/12/31 |
| Number | 1,234.56 | 1.234,56 | 1,234.56 |
| Currency | $99.99 | 99,99 € | ¥100 |
| Date | 12/31/2024 | 31.12.2024 | 2024/12/31 |
|---|---|---|---|
| Number | 1,234.56 | 1.234,56 | 1,234.56 |
| Currency | $99.99 | 99,99 € | ¥100 |
| Date | 12/31/2024 | 31.12.2024 | 2024/12/31 |
|---|---|---|---|
| Number | 1,234.56 | 1.234,56 | 1,234.56 |
| Currency | $99.99 | 99,99 € | ¥100 |
| Number | 1,234.56 | 1.234,56 | 1,234.56 |
|---|---|---|---|
| Currency | $99.99 | 99,99 € | ¥100 |
| Currency | $99.99 | 99,99 € | ¥100 |
|---|
7. Handle Pluralization Properly
Different languages have different plural rules:
| Language | Plural Forms |
|---|---|
| English | 2 (one, other) |
| French | 2+ (one, other, many) |
| Russian | 3 (one, few, many) |
| Arabic | 6 (zero, one, two, few, many, other) |
| English | 2 (one, other) |
|---|---|
| French | 2+ (one, other, many) |
| Russian | 3 (one, few, many) |
| Arabic | 6 (zero, one, two, few, many, other) |
| English | 2 (one, other) |
|---|---|
| French | 2+ (one, other, many) |
| Russian | 3 (one, few, many) |
| Arabic | 6 (zero, one, two, few, many, other) |
| French | 2+ (one, other, many) |
|---|---|
| Russian | 3 (one, few, many) |
| Arabic | 6 (zero, one, two, few, many, other) |
| Russian | 3 (one, few, many) |
|---|---|
| Arabic | 6 (zero, one, two, few, many, other) |
| Arabic | 6 (zero, one, two, few, many, other) |
|---|
// Use ICU plural format
t('items', { count: 5 })
// Config handles the rules per language8. Support RTL Languages
Arabic, Hebrew, Persian, and Urdu read right-to-left:
/* Use logical properties */
.card {
margin-inline-start: 1rem; /* Not margin-left */
padding-inline-end: 1rem; /* Not padding-right */
}
/* Let dir attribute handle layout */
[dir="rtl"] .icon {
transform: scaleX(-1);
}<html dir={isRTL ? 'rtl' : 'ltr'}>Translation Quality
9. Provide Context for Translators
Translators need to know where and how strings are used:
{
"save": {
"value": "Save",
"context": "Button to save user profile changes, appears at bottom of form",
"maxLength": 10,
"screenshot": "save-button.png"
}
}10. Use a Glossary
Maintain consistent terminology across translations:
| English | Spanish | German | Context |
|---|---|---|---|
| Dashboard | Panel de control | Dashboard | Main user interface |
| Settings | Configuración | Einstellungen | User preferences |
| Submit | Enviar | Absenden | Form submission button |
| Dashboard | Panel de control | Dashboard | Main user interface |
|---|---|---|---|
| Settings | Configuración | Einstellungen | User preferences |
| Submit | Enviar | Absenden | Form submission button |
| Dashboard | Panel de control | Dashboard | Main user interface |
|---|---|---|---|
| Settings | Configuración | Einstellungen | User preferences |
| Submit | Enviar | Absenden | Form submission button |
| Settings | Configuración | Einstellungen | User preferences |
|---|---|---|---|
| Submit | Enviar | Absenden | Form submission button |
| Submit | Enviar | Absenden | Form submission button |
|---|
11. Leverage Translation Memory
Don't translate the same phrase twice. TM catches:
ROI: 30-60% reduction in translation costs over time.
Process
12. Automate String Extraction
Manual extraction misses strings and wastes time:
# Scan codebase weekly in CI
npx intlpull scan --push
# Transform found strings automatically
npx intlpull scan --apply13. Use Continuous Localization
Don't batch translations. Localize continuously:
# GitHub Action example
on:
push:
branches: [main]
paths:
- 'src/**'
- 'locales/en.json'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npx intlpull push
- run: npx intlpull pull
- uses: stefanzweifel/git-auto-commit-action@v414. Test with Pseudo-Localization
Before real translations, test with pseudo-translations:
"Welcome" → "[Ẃéĺćőḿé !!!!!!]"Pseudo-localization reveals:
15. Implement OTA Updates for Mobile
Don't wait for app store releases to fix translations:
// With OTA, translation updates are instant
let message = IntlPull.t("welcome.message")
// Change in dashboard → users see it immediatelyWithout OTA: 1-2 week delay for any translation change
With OTA: Instant updates, no app release needed
Anti-Patterns to Avoid
Don't: Use String Concatenation
// Never do this
"Welcome, " + nameDon't: Hardcode Formatting
// Never do this
date.toLocaleDateString('en-US')Don't: Split Sentences
// Never do this
<span>{t('click')}</span>
<Link>{t('here')}</Link>
<span>{t('to_continue')}</span>Don't: Assume Language = Country
// Never do this
if (country === 'US') language = 'en';
// Spanish speakers in US, Portuguese in US, etc.Don't: Forget About Text Expansion
/* Never do this */
.button {
width: 100px; /* Breaks in German */
}Checklist
Before launching in a new language:
Tools That Help
| Need | Tool |
|---|---|
| Translation Management | IntlPull |
| String Extraction | IntlPull CLI |
| Code Transformation | IntlPull CLI |
| AI Translation | IntlPull (GPT-4, Claude) |
| OTA Updates | IntlPull SDK |
| Pseudo-localization | IntlPull |
| Translation Management | IntlPull |
|---|---|
| String Extraction | IntlPull CLI |
| Code Transformation | IntlPull CLI |
| AI Translation | IntlPull (GPT-4, Claude) |
| OTA Updates | IntlPull SDK |
| Pseudo-localization | IntlPull |
| Translation Management | IntlPull |
|---|---|
| String Extraction | IntlPull CLI |
| Code Transformation | IntlPull CLI |
| AI Translation | IntlPull (GPT-4, Claude) |
| OTA Updates | IntlPull SDK |
| Pseudo-localization | IntlPull |
| String Extraction | IntlPull CLI |
|---|---|
| Code Transformation | IntlPull CLI |
| AI Translation | IntlPull (GPT-4, Claude) |
| OTA Updates | IntlPull SDK |
| Pseudo-localization | IntlPull |
| Code Transformation | IntlPull CLI |
|---|---|
| AI Translation | IntlPull (GPT-4, Claude) |
| OTA Updates | IntlPull SDK |
| Pseudo-localization | IntlPull |
| AI Translation | IntlPull (GPT-4, Claude) |
|---|---|
| OTA Updates | IntlPull SDK |
| Pseudo-localization | IntlPull |
| OTA Updates | IntlPull SDK |
|---|---|
| Pseudo-localization | IntlPull |
| Pseudo-localization | IntlPull |
|---|
Conclusion
Good i18n is about:
Follow these 15 practices and you'll build apps that scale globally without the typical i18n pain.
Ready to level up your i18n? Try IntlPull free and implement these best practices with the right tools.