Quick answer: The core i18n best practices are: extract all user-facing strings from day one, never concatenate or split sentences, use ICU MessageFormat for plurals and gender, keep keys semantic, rely on native Intl formatting APIs for dates/numbers/currency, support RTL with logical CSS properties, give translators context, and automate translation sync with continuous localization and pseudo-localization testing. Follow these and your app scales globally without the typical i18n pain.
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.
JavaScript1// Bad - hardcoded string 2<button>Submit</button> 3 4// Good - extracted from start 5<button>{t('form.submit')}</button>
Tip: Use IntlPull's CLI to check translation status:
Terminalnpx @intlpullhq/cli status
2. Never Concatenate Strings
Different languages have different word orders. Concatenation breaks translations.
JavaScript1// Bad - breaks in German, Japanese, Arabic... 2const message = "Hello " + name + ", you have " + count + " messages"; 3 4// Good - uses interpolation 5const message = t('greeting', { name, count }); 6// en: "Hello {name}, you have {count} messages" 7// de: "Hallo {name}, Sie haben {count} Nachrichten" 8// 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:
JavaScript1// Bad - tied to UI location 2"homepage_header_title" 3"sidebar_menu_item_3" 4 5// Good - describes content 6"welcome.title" 7"navigation.settings" 8"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:
JavaScript1// Bad - US-only format 2const date = `${month}/${day}/${year}`; 3const price = `$${amount}`; 4 5// Good - locale-aware 6const date = new Intl.DateTimeFormat(locale).format(dateObj); 7const price = new Intl.NumberFormat(locale, { 8 style: 'currency', 9 currency: userCurrency 10}).format(amount);
Locale differences:
| US | Germany | Japan | |
|---|---|---|---|
| Date | 12/31/2026 | 31.12.2026 | 2026/12/31 |
| Number | 1,234.56 | 1.234,56 | 1,234.56 |
| 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) |
JavaScript// Use ICU plural format t('items', { count: 5 }) // Config handles the rules per language
8. Support RTL Languages
Arabic, Hebrew, Persian, and Urdu read right-to-left:
CSS1/* Use logical properties */ 2.card { 3 margin-inline-start: 1rem; /* Not margin-left */ 4 padding-inline-end: 1rem; /* Not padding-right */ 5} 6 7/* Let dir attribute handle layout */ 8[dir="rtl"] .icon { 9 transform: scaleX(-1); 10}
JSX<html dir={isRTL ? 'rtl' : 'ltr'}>
Translation Quality
9. Provide Context for Translators
Translators need to know where and how strings are used:
JSON1{ 2 "save": { 3 "value": "Save", 4 "context": "Button to save user profile changes, appears at bottom of form", 5 "maxLength": 10, 6 "screenshot": "save-button.png" 7 } 8}
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 |
11. Leverage Translation Memory
Don't translate the same phrase twice. TM catches:
- Exact matches: "Save changes" = "Save changes"
- Fuzzy matches: "Save changes" ≈ "Save all changes"
ROI: 30-60% reduction in translation costs over time.
Process
12. Automate Translation Sync
Manual syncing wastes time and leads to outdated translations:
Terminal1# Sync translations in CI 2npx @intlpullhq/cli upload 3npx @intlpullhq/cli download 4 5# Or use watch mode for local development 6npx @intlpullhq/cli sync --watch
13. Use Continuous Localization
Don't batch translations. Localize continuously:
YAML1# GitHub Action example 2on: 3 push: 4 branches: [main] 5 paths: 6 - 'src/**' 7 - 'locales/en.json' 8 9jobs: 10 sync: 11 runs-on: ubuntu-latest 12 steps: 13 - uses: actions/checkout@v3 14 - run: npx @intlpullhq/cli upload 15 - run: npx @intlpullhq/cli download 16 - uses: stefanzweifel/git-auto-commit-action@v4
14. Test with Pseudo-Localization
Before real translations, test with pseudo-translations:
"Welcome" → "[Ẃéĺćőḿé !!!!!!]"
Pseudo-localization reveals:
- Hardcoded strings (not transformed)
- Layout issues (text expansion)
- Truncation problems
- Character encoding issues
15. Implement OTA Updates for Mobile
Don't wait for app store releases to fix translations:
Swift// With OTA, translation updates are instant let message = IntlPull.t("welcome.message") // Change in dashboard → users see it immediately
Without 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
JavaScript// Never do this "Welcome, " + name
Don't: Hardcode Formatting
JavaScript// Never do this date.toLocaleDateString('en-US')
Don't: Split Sentences
JavaScript1// Never do this 2<span>{t('click')}</span> 3<Link>{t('here')}</Link> 4<span>{t('to_continue')}</span>
Don't: Assume Language = Country
JavaScript// Never do this if (country === 'US') language = 'en'; // Spanish speakers in US, Portuguese in US, etc.
Don't: Forget About Text Expansion
CSS1/* Never do this */ 2.button { 3 width: 100px; /* Breaks in German */ 4}
Checklist
Before launching in a new language:
- All strings extracted and translated
- Pluralization tested
- Date/number/currency formatting correct
- RTL layout works (if applicable)
- Images and icons localized
- SEO metadata translated
- Error messages translated
- Email templates translated
- Legal pages translated
- Tested by native speakers
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 |
Frequently Asked Questions
What are the most important i18n best practices?
The most important i18n best practices are extracting all user-facing strings early, never concatenating or splitting sentences, using ICU MessageFormat for plurals and gender, and relying on native Intl APIs for date, number, and currency formatting. Together these prevent the bugs that are hardest to fix retroactively—hardcoded strings, broken word order, and locale-specific formatting errors. Pair them with translator context and automated sync for a workflow that scales.
Why should I extract strings before I need translation?
You should extract strings before you need translation because retrofitting is exponentially harder—by the time you "need" translation, you may have thousands of hardcoded strings scattered across components. Extracting from day one keeps each string in one place with a semantic key, so adding a language later is a content task instead of a code refactor. It also lets you run pseudo-localization early to catch layout and truncation issues.
How do I handle pluralization across languages?
Handle pluralization with ICU MessageFormat rather than if (count === 1) checks, because plural rules vary widely—English has 2 forms, Russian 3, and Arabic 6. ICU lets you declare each plural category (zero, one, two, few, many, other) and the runtime selects the correct one per locale. Store the plural message as a single key so translators provide all required forms for their language.
Should I use string concatenation for dynamic messages?
No, you should never concatenate strings for dynamic messages because word order differs between languages and concatenation locks in the source-language order. Use interpolation with named placeholders instead—t('greeting', { name, count })—so each translation can reposition the variables naturally. This single rule prevents a large class of untranslatable strings.
How do I test internationalization before translating?
Test internationalization with pseudo-localization before real translation by transforming source strings into accented, expanded versions like "[Ẃéĺćőḿé !!!!!!]". Any string that renders unchanged is hardcoded and was missed during extraction, while the added length reveals layout, truncation, and text-expansion problems. Run it in CI so regressions are caught on every build.
Does language always map to country?
No, language does not map to country, and assuming it does breaks the experience for many users—there are Spanish and Portuguese speakers in the US, multiple official languages in countries like Switzerland and India, and migrants everywhere. Detect language from the user's explicit preference or the Accept-Language header rather than from geolocation, and let users override it. Treat locale (language plus region) as the unit, not country alone.
Conclusion
Good i18n is about:
- Clean architecture: Extract strings early, use interpolation
- Proper formatting: Let APIs handle locale differences
- Quality translations: Context, glossaries, TM
- Smart process: Automation, continuous flow, testing
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.

