Implementing i18n
Expert guidance for building applications that work across languages, regions, and writing directions.
Capabilities
- String externalization -- Extract hardcoded user-facing strings into translation files with interpolation support
- Pluralization -- Implement correct plural forms across languages with varying grammatical rules
- Locale-aware formatting -- Format dates, times, numbers, and currencies using
IntlAPIs or equivalent libraries - RTL layout support -- Adapt CSS and HTML for right-to-left scripts using logical properties and direction attributes
- Translation file architecture -- Design scalable, namespace-organized translation file structures
- Translation workflow -- Set up end-to-end pipelines for extracting, translating, reviewing, and importing strings
- Library selection -- Choose the right i18n library for the framework and use case (i18next, react-intl, vue-i18n, gettext, fluent)
- Anti-pattern detection -- Identify and correct common i18n mistakes such as string concatenation, hardcoded formats, and text in images
Core Principles
1. Externalize Everything
No user-facing string should be hardcoded. All visible text must flow through a translation function with support for interpolation and context.
2. Use Platform APIs for Formatting
Never hand-roll date, number, or currency formatting. Use Intl.DateTimeFormat, Intl.NumberFormat, and equivalent APIs that respect locale conventions automatically.
3. Design for Text Expansion
Translated text can be 30-200% longer than English. Layouts must accommodate variable-length strings without breaking. Never assume fixed text widths.
4. Logical Properties Over Physical
CSS should use logical properties (margin-inline-start, padding-inline-end) instead of physical ones (margin-left, padding-right) so layouts adapt automatically for RTL scripts.
5. Respect Plural Rules
Different languages have different plural categories (English has 2, Russian has 4, Arabic has 6). Use ICU plural syntax or library-specific plural handling -- never assume "singular vs plural" is sufficient.
6. Provide Translator Context
Translation keys should be descriptive. Include comments or context strings so translators understand where and how text appears. Avoid reusing the same key for different UI contexts.
7. Namespace by Feature
Organize translation files by feature or page (e.g., common.json, dashboard.json, errors.json) to keep files manageable and enable lazy loading of translations.
Workflow
Implementing i18n in a Project
1. Audit Existing Strings
- Scan the codebase for hardcoded user-facing strings
- Identify date, number, and currency formatting that bypasses locale
- Note any physical CSS properties that break under RTL
- Catalog images containing embedded text
2. Choose an i18n Library
| Library | Platform | Notes |
|---|---|---|
i18next |
JS/React | Feature-rich, extensive plugin ecosystem |
react-intl |
React | Part of FormatJS ecosystem, ICU message syntax |
vue-i18n |
Vue | Official Vue solution, deep framework integration |
gettext |
Python | Classic approach, widespread tooling support |
fluent |
Mozilla | Advanced grammar handling, asymmetric localization |
3. Set Up Translation File Structure
locales/
en/
common.json
errors.json
dashboard.json
es/
common.json
errors.json
dashboard.json
ar/
...
4. Externalize Strings
// BAD: Hardcoded strings
const message = "Welcome back, " + userName + "!";
// GOOD: Externalized with interpolation
const message = t('welcome_back', { name: userName });
// en.json: { "welcome_back": "Welcome back, {{name}}!" }
// es.json: { "welcome_back": "Bienvenido de nuevo, {{name}}!" }
5. Implement Pluralization
// en.json
{
"items_count": {
"zero": "No items",
"one": "{{count}} item",
"other": "{{count}} items"
}
}
// ru.json (more plural forms)
{
"items_count": {
"one": "{{count}} \u044d\u043b\u0435\u043c\u0435\u043d\u0442",
"few": "{{count}} \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430",
"many": "{{count}} \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432",
"other": "{{count}} \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430"
}
}
6. Apply Locale-Aware Formatting
// Date formatting
const dateFormatter = new Intl.DateTimeFormat('de-DE', {
dateStyle: 'long',
timeStyle: 'short',
});
dateFormatter.format(new Date()); // "30. Januar 2026 um 08:45"
// Number formatting
new Intl.NumberFormat('de-DE').format(1234567.89);
// "1.234.567,89"
// Currency formatting
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY',
}).format(1000);
// "\uffe51,000"
7. Implement RTL Support
/* Use logical properties instead of physical */
.card {
/* BAD: Physical properties */
margin-left: 1rem;
padding-right: 2rem;
/* GOOD: Logical properties */
margin-inline-start: 1rem;
padding-inline-end: 2rem;
}
/* Set document direction */
html[dir="rtl"] {
direction: rtl;
}
8. Establish Translation Pipeline
- Extract -- Scan code for translatable strings and generate key files
- Translate -- Send to translators via platform (Crowdin, Lokalise, Phrase)
- Review -- Native speaker QA for accuracy and context
- Import -- Pull translations into the codebase
- Test -- Verify in-context with pseudo-localization and visual review
Quick Reference -- Anti-Patterns
| Avoid | Instead |
|---|---|
| String concatenation for messages | Template interpolation with t() |
| Hardcoded date/number formats | Intl APIs or library formatters |
| Assuming fixed text length | Flexible, responsive layouts |
| Images with embedded text | Separate text layer or CSS text overlay |
en as implicit fallback for en-GB |
Explicit fallback chain configuration |
| Same key for different UI contexts | Distinct keys with translator comments |
| Singular/plural only | Full ICU plural categories per language |
Quick Reference -- Key Terms
| Term | Meaning |
|---|---|
| i18n | Internationalization -- making code locale-aware |
| l10n | Localization -- adapting content for a specific locale |
| Locale | Language + region identifier (e.g., en-US, fr-CA) |
| RTL | Right-to-left scripts (Arabic, Hebrew, Farsi) |
| ICU | International Components for Unicode -- standard message format |
| CLDR | Common Locale Data Repository -- reference locale data |
Checklist
- No hardcoded user-facing strings in the codebase
- All strings externalized to namespaced translation files
- Pluralization handled with correct plural categories per language
- Dates, numbers, and currencies formatted via
IntlAPIs or i18n library - CSS uses logical properties (
inline-start/end,block-start/end) - RTL layout tested with Arabic or Hebrew locale (if applicable)
- Translation files complete with no missing keys across supported locales
- Translator context provided for ambiguous or context-dependent strings
- Pseudo-localization tested to catch layout overflow and truncation issues
- Fallback chain configured (e.g.,
fr-CA->fr->en) - Translation pipeline integrated into development workflow
- Images reviewed for embedded text; text separated where found
- Text expansion accommodated in UI components (30-200% growth)
When to Escalate
- Bidirectional text mixing -- Complex cases where LTR and RTL content coexist in the same paragraph or input field require specialist review
- Legal or regulated content -- Translations of legal terms, medical content, or financial disclosures require certified human translators, not machine translation
- New script support -- Adding support for CJK, Indic, or other complex scripts may require font loading strategies and specialized rendering review
- Locale data conflicts -- When CLDR data differs from business requirements (e.g., custom date formats for a specific market), escalate to product for a decision
- Performance concerns -- If loading all translation bundles degrades initial load time, escalate to architecture review for lazy loading or CDN-based translation delivery
- Accessibility intersection -- When i18n changes affect screen reader behavior or ARIA labels across languages, coordinate with accessibility review
Converted and distributed by TomeVault — claim your Tome and manage your conversions.