Two concepts with similar names and very different meanings. Internationalisation is what you do once, in the codebase, to make a product capable of supporting multiple languages and regions. Localisation is what you do repeatedly, for each language, to make the product actually speak that language. Test both, and test them early — localisation bugs caught before translation begins cost minutes; the same bugs caught after a full translation costs days.
i18n vs l10n: the distinction
Internationalisation (i18n) — the engineering foundation. An internationalised codebase stores no text directly in the source code; it fetches all user-visible strings from translation files. It uses locale-aware APIs for dates, numbers, and currencies rather than hardcoding formats. It supports character sets beyond ASCII. It can display right-to-left text without layout breaking. i18n enables l10n to happen.
Localisation (l10n) — the per-language work. Translations are written, cultural conventions are applied, region-specific content (legal notices, currency, phone number formats) is adapted. l10n builds on i18n infrastructure; without i18n, l10n means manually editing source code for every supported language.
The abbreviations: i18n is "internationalization" — 18 letters between i and n. l10n is "localization" — 10 letters between l and n.
What an internationalised vs non-internationalised codebase looks like
A hardcoded string in source code:
// Not internationalised — will always say "Submit" regardless of locale
<button>Submit</button>An internationalised string:
// Internationalised — fetches from translation file for the active locale
<button>{t('form.submit')}</button>Testing i18n means checking that every visible string comes from the translation layer — not that it is correctly translated into a specific language. If changing the locale from en to de causes some strings to switch to German and others to remain in English, those remaining-English strings are hardcoded: an i18n bug.
What changes across locales
The same form in four locales — what each requires
English (en-US)
Date: 12/31/2024 (MM/DD/YYYY)
Number: 1,234.56
Currency: $1,234.56
Text direction: left-to-right
Text length: baseline (100%)
German (de-DE)
Date: 31.12.2024 (DD.MM.YYYY)
Number: 1.234,56 (dots and commas swap!)
Currency: 1.234,56 € (symbol at end)
Text direction: left-to-right
Text length: ~30–40% longer than English
Arabic (ar)
Date: varies by calendar system
Number: may use Eastern Arabic numerals
Text direction: right-to-left
Entire layout must mirror
Icons with direction must flip
Japanese (ja)
Date: 2024年12月31日 (year-month-day)
Number: ¥1,234 (no decimal in yen)
Character set: kanji, hiragana, katakana
Text length: often shorter than English
Font requirements: significantly different
Text expansion and layout
German, French, Finnish, and Russian text is typically 20–40% longer than the equivalent English text. A button labelled "Submit" (6 characters) becomes "Einreichen" (10 characters) in German — almost twice as wide.
Layouts that look clean in English overflow, break, or clip in longer languages. This is one of the most common and most preventable i18n bugs:
- Fixed-width buttons that cannot accommodate longer labels
- Navigation items that wrap unexpectedly
- Table columns that overflow their container
- Truncated text with ellipsis that cuts meaningful content
The fix is flexible, responsive layouts — not fixed-pixel widths for text containers.
Right-to-left (RTL) languages
Arabic, Hebrew, Persian, and Urdu are written right-to-left. An internationalised product that supports these languages must mirror its entire layout when an RTL locale is active:
- Navigation moves from left to right
- Text aligns right
- Icons that imply direction (forward/back arrows, progress indicators) flip
- The overall visual flow is mirrored
This is not achieved by simply flipping text alignment. It requires setting dir="rtl" on the HTML element and using CSS logical properties (margin-inline-start instead of margin-left) throughout the stylesheet. Testing RTL: switch the OS or browser language to Arabic or Hebrew and verify the full layout mirrors correctly.
Date, number, and currency formats
These are not translation issues — they are data formatting issues that must be handled programmatically:
| Data type | en-US | en-GB | de-DE | ja-JP |
|---|---|---|---|---|
| Date | 12/31/2024 | 31/12/2024 | 31.12.2024 | 2024年12月31日 |
| Number | 1,234.56 | 1,234.56 | 1.234,56 | 1,234.56 |
| Currency | $1,234.56 | £1,234.56 | 1.234,56 € | ¥1,235 |
Hardcoding any format causes bugs in other locales. The solution is locale-aware formatting via platform APIs: Intl.DateTimeFormat and Intl.NumberFormat in JavaScript, DateTimeFormatter in Java, strftime with locale in Python.
Pseudo-localisation
Pseudo-localisation replaces actual translation with a modified version of the source text that simulates translation effects. A common approach: replace ASCII characters with visually similar accented variants and pad strings to simulate expansion.
"Submit" → "[Šüβmíť @@@@]"
This immediately reveals:
- Hardcoded strings — still appear in plain English
- Layout issues — longer padded strings break containers without real translation effort
- Encoding problems — special characters that do not render correctly
- Missing string keys — a key like
form.submit.labelthat was never added to translation files shows as[form.submit.label]
Pseudo-localisation should be run during development, not only before release. It catches 80% of i18n bugs when they are cheapest to fix.
Common localisation bugs
- Mixed-language pages: some strings translated, others not (hardcoded or missing translation key)
- Date formatted as MM/DD for a UK user expecting DD/MM
- Currency symbol displayed in wrong position (100$ instead of $100)
- Translation key displayed instead of translated text (
user.profile.titleinstead of "Profile") - Buttons or labels cut off because translated text is longer than the container
- Numbers using wrong separator (decimal point vs decimal comma)
- Error messages and email templates untranslated even though the UI is localised
⚠️ Common mistakes
- Adding i18n as an afterthought. Retrofitting internationalisation into a codebase built with hardcoded strings requires touching every string in the application. Building i18n from the start is a fraction of the work. Even if you have no non-English users today, the investment makes future localisation tractable.
- Treating machine translation as a QA shortcut. Machine-translated strings can be used for layout testing (spotting expansion issues, missing keys), but not for release. Machine translations produce technically correct but culturally wrong output — and in some languages, genuinely offensive phrasing. Native-speaker review is required before any locale ships to users.
- Testing only the UI, not emails and documents. Localisation bugs in transactional emails, PDF invoices, and in-app notifications are common because these are easy to forget. Every user-facing string in the product requires a translation entry.
🎯 Practice task
Test the i18n readiness of a product you have access to.
- Change your browser or OS language to German (de-DE) or French (fr-FR) and reload the application. Are all strings translated? Any still in English?
- Find a date, a number, and a currency displayed on any page. Are they formatted for the active locale, or in a fixed format regardless of locale?
- If you have a staging environment with translation file access, add pseudo-localisation to one page's strings and verify: are any strings still appearing in English? Does any element overflow or clip?
Document each finding with the locale, the page, the element, what was expected, and what appeared. These findings directly inform the internationalisation work required before a new locale can be supported.