Use when designing or implementing internationalization — string externalization, locale-aware formatting, pluralization, text expansion, concatenation avoidance, and layout adaptation for multiple languages. Trigger when the user mentions i18n, internationalization, localization, translation, locale, or multi-language.
I18n is preparing the codebase so that adding a new language doesn't require rewriting the UI. L10n is the act of adding one.
No user-visible string lives in source code. Every string goes through a translation function.
// Bad
<button>Save changes</button>
// Good
<button>{t('actions.save_changes')}</button>
Keys are structured, not sequential: actions.save_changes not string_47.
German is 30–40% longer than English. Finnish, Russian, and Arabic can be longer still. Japanese and Chinese are often shorter.
| Language | Expansion vs English |
|---|---|
| German | +30–40% |
| French |
| +15–25% |
| Russian | +20–30% |
| Japanese | -20–30% |
| Arabic | +20–30% |
Design for 40% expansion. If a label barely fits in English, it breaks in German.
Test with pseudo-localization: replace strings with accented, expanded versions ([Ŝàvé çhàñgéŝ!!!!]) to catch overflow before real translations arrive.
// Bad — word order varies by language
`Welcome, ${name}. You have ${count} messages.`
// Good — use ICU message format
t('welcome', { name, count })
// en: "Welcome, {name}. You have {count, plural, one {# message} other {# messages}}."
// ja: "{name}さん、{count}件のメッセージがあります。"
Concatenation assumes English word order. Interpolation with named variables lets translators reorder.
English has 2 plural forms (singular, plural). Arabic has 6. Polish has 4. Use ICU {count, plural, ...} for every countable string.
{count, plural,
=0 {No messages}
one {# message}
other {# messages}
}
Libraries: intl-messageformat, i18next with ICU plugin, FormatJS.
Use Intl APIs. Never manually format.
new Intl.DateTimeFormat('de-DE', { dateStyle: 'long' }).format(date);
// "18. April 2026"
new Intl.NumberFormat('en-IN').format(1234567);
// "12,34,567"
Intl.DateTimeFormat. Month/day order varies by locale.Intl.NumberFormat. Thousands/decimal separators vary.Intl.NumberFormat with style: 'currency'. Symbol position varies.Intl.RelativeTimeFormat. "3 days ago" / "vor 3 Tagen".font-feature-settings won't break Arabic, but custom letter-spacing will).canon-typography apply to Latin/Cyrillic only.min-width, not width.| Anti-pattern | Why it fails |
|---|---|
| Hardcoded strings in JSX/HTML | Untranslatable |
| String concatenation for sentences | Word order varies by language |
count === 1 ? 'item' : 'items' | Wrong for Arabic, Polish, Russian, etc. |
| Manual date formatting | Month/day order varies |
| Fixed-width buttons with English text | German breaks them |
| Letter-spacing on Arabic text | Breaks connected script |
| One font stack for all scripts | Missing glyphs |
| Using flags for language switcher | Flags represent countries, not languages (Spanish is spoken in 20+ countries) |
Intl APIscanon-rtl if targeting Arabic/Hebrew