10 common i18n mistakes and how to avoid them
Learn the most common internationalization mistakes developers make, and how to fix them. Improve your app's localization quality with these best practices.
Internationalization (i18n) seems straightforward — until you discover that your German users see truncated buttons, your Japanese users receive mangled sentence fragments, and your Arabic-speaking users face completely broken layouts. These aren’t edge cases. They’re the predictable consequences of common mistakes that most development teams make when tackling localization for the first time. In this guide, we walk through the 10 most common i18n mistakes, explain exactly why they happen, and show you step by step, how to fix each one. Whether you’re building a new app or upgrading an existing one — avoiding these pitfalls will save you weeks of debugging.
Error #1: Hard-coded strings
Hard-coding strings is the most fundamental i18n mistake. It happens because developers focus first on getting features running, and plan to 'clean it up later'. But later never comes, and suddenly you have thousands of strings spread across hundreds of files.
['Set up your i18n framework before you write the first line of UI code', 'Use a linter plugin to detect hard-coded strings in templates and components', 'Keep translation keys descriptive and organized by feature or page']
Set up your i18n framework before writing the first line of UI code.
Use a linter plugin to detect hard-coded strings in templates and components.
Keep translation keys descriptive and organized by feature or page.
A Typical Scenario
A developer writes a button label directly in JSX: <button>Submit Order</button>.
The app ships in English and works perfectly. Six months later, the company expands into Germany.
The localization team discovers 2,000+ hard-coded strings. The retrofit takes three weeks and causes 47 bugs.
Why this is a problem
In a mature codebase, hard-coded strings can run into the thousands. Extracting them later requires touching every file, retesting every component, and risking regressions everywhere.
Hard-coded strings are embedded directly in source code, templates, or components. They cannot be extracted, translated, or swapped at runtime without changing the code itself.
Users in non-English locales see untranslated UI elements mixed with translated content — a chaotic and unprofessional impression.
How to fix it
Externalize all user-visible strings from the start into resource files.
Set up a translation framework (e.g., next-intl, react-i18next, Vue-i18n) before you write your first component.
Create a resource file structure (e.g., messages/de.json) and reference all strings via translation keys like t('checkout.submitButton').
Add a linting rule or a pre-commit hook that flags raw string literals in UI components.
Error #10: Translate everything literally
Not all content should be translated. Brand names, names of legal entities, technical terms, and certain product names must remain in their original language. Overzealous translation can cause legal issues, brand inconsistency, and user confusion.
["Maintain a 'Do not translate' glossary and share it with all translators", 'Use locked segments or separate namespaces for brand and legal terms', 'Always provide context notes for ambiguous strings to prevent mis-translation']
Maintain a 'do-not-translate' glossary and share it with all translators
Use locked segments or separate namespaces for brand and legal terms
Always provide context notes for ambiguous strings to prevent mistranslations
A typical scenario
A translation file contains the company name 'CloudForge Inc.' and the technical term 'OAuth 2.0 Token' as regular translatable strings.
The Spanish translator translates 'CloudForge' to 'ForjaNube' and 'OAuth 2.0 Token' to 'ficha OAuth 2.0'.
Result: users cannot find the company under its real name, and developers reading the Spanish documentation are confused by unfamiliar translated terms.
Why this is a problem
A single incorrectly translated brand name in a legal document can invalidate contracts. Correcting over-translation requires reviewing every string in every language — a task that can take several weeks.
If strings are sent to translation without context, translators may translate brand names ('Apple' → 'Apfel'), legal terms ('GmbH' → 'LLC') or technical terms that must remain in English.
Users will not find products under their well-known brand names, legal documents reference the wrong company, and technical documentation becomes incomprehensible when terms such as 'API Endpoint' are translated.
How to fix it
Clearly mark non-translatable content and provide context notes for translators.
Create a 'Do not translate' glossary that lists all brand names, product names, corporate entities, and technical terms that must remain unchanged.
Use a separate namespace or special keys for non-translatable content. Many i18n tools support 'locked' segments that translators cannot edit.
Add translator comments/descriptions to your translation files that explain the context: 'This is a brand name — do not translate' or 'Technical term — leave in English'.
Mistake #2: String concatenation
Joining sentences from fragments may read logically in English, but it doesn’t work in other languages. Word order, grammar, and sentence structure vary dramatically between languages, making concatenated strings untranslatable.
['Never concatenate translated fragments into sentences', 'Use named placeholders ('{name}') instead of positional ({0}) for clarity', 'Provide context comments for translators explaining what each placeholder contains']
Never build sentences by concatenating translated fragments.
Use named placeholders ('{name}') instead of positional ({0}) for clarity
Provide context comments for translators that explain what each placeholder contains
A typical scenario
Developer writes: 'You have ' + count + ' items in your ' + cartType + ' cart' — works perfectly in English.
The German translator receives three separate fragments and cannot form a grammatically correct sentence because the word order must change.
Result: German users see 'You have 5 items in your Standard cart' — clumsy and unprofessional.
Why this is a problem
Every concatenated string is a ticking time bomb. With 20 languages and 50 concatenated strings, you have 1,000 potential grammar errors that must be fixed manually.
String concatenation such as 'Welcome ' + userName + ', you have ' + count + ' new messages' requires a specific word order. Translators receive incoherent fragments that they cannot rearrange.
Users see grammatically incorrect sentences. In German the verb is often at the end. In Arabic the entire structure is reversed. The result reads like nonsense.
How to fix it
Use parameterized messages with named placeholders so translators can rearrange the entire sentence.
Replace concatenation with a single message key with placeholders: 'cart.summary': 'You have {count} items in your '{cartType}' cart'.
Pass variables as parameters to your translation function: t('cart.summary', { count: 5, cartType: 'Premium' }).
Translators can now freely rearrange: 'In deinem '{cartType}'-Warenkorb befinden sich {count} Artikel' — grammatically correct German.
Error #3: Ignoring pluralization
English has two plural forms: singular and plural. Developers often assume all languages work the same. They don't. Polish has four forms, Arabic has six, and even languages like French treat zero differently from English.
['Always use ICU MessageFormat or an equivalent library for pluralizable content', 'Never write your own plural logic — rely on the CLDR rules', 'Test plurals with values such as 0, 1, 2, 5, 21 and 100 to cover all categories']
Always use ICU MessageFormat or an equivalent library for pluralizable content
Never write your own plural logic—trust the CLDR rules
Test plurals with values such as 0, 1, 2, 5, 21 and 100 to cover all categories
A typical scenario
Developer writes: count + (count === 1 ? ' file' : ' files') — handles German correctly.
The Polish translator needs 4 forms: 1 plik, 2-4 pliki, 5-21 plików, 22-24 pliki. The simple ternary cannot express that.
Result: Polish users see '5 pliki' (wrong form) instead of '5 plików' (correct form), and the app looks broken.
Why this is a problem
Every countable noun in your app needs plural handling. With 50 such strings and 20 languages that's 1,000 plural rules — impossible to manage manually.
Simple if/else checks (count === 1 ? 'file' : 'files') handle only German and English. The CLDR defines up to 6 plural categories: zero, one, two, few, many, and other. Each language uses a different subset.
Users see grammatically incorrect text like '1 messages' or completely wrong plural forms. In formal contexts, this undermines credibility.
How to fix it
Use ICU MessageFormat, which supports all CLDR plural rules out of the box.
Define messages with ICU syntax: 'fileCount': '{count, plural, one {# file} other {# files}}'.
Translators provide all required plural forms for their language. Polish: '{count, plural, one {# plik} few {# pliki} many {# plików} other {# pliku}}'.
Your i18n library automatically selects the correct form at runtime based on the active locale's CLDR rules.
Mistake #4: Fixed UI element widths
Designers create pixel-perfect layouts in English, and developers implement them with fixed widths. But translated text can be dramatically longer or shorter. German text is about 30% longer than English, while Chinese text can be 50% shorter.
['Never use fixed pixel widths for elements with translatable text', 'Plan for a baseline of 40% text expansion — some languages expand even more', 'Use CSS Flexbox or Grid for layouts that adapt to varying content lengths']
Never use fixed pixel widths for elements with translatable text
Plan for a 40% text expansion as a baseline—some languages expand even more
Use CSS Flexbox or Grid for layouts that adapt to varying content lengths
A typical scenario
The designer creates a navigation bar with five buttons, each exactly 100px wide — looks great in English.
German translation: 'Settings' becomes 'Einstellungen' (13 vs 8 characters), 'Submit' becomes 'Absenden' (8 vs 6). The navbar overflows.
Result: On mobile devices, buttons stack on top of each other or text is cut off, making navigation unusable for German users.
Why this is a problem
Every element with a fixed width is a potential breaking point. A typical app has hundreds of buttons, labels and cards that all need to accommodate text expansion.
Container with fixed width (width: 120px) and buttons of fixed size clip or overflow when text expands. CSS overflow: hidden silently hides content, while overflow: visible breaks the layout.
Users see truncated labels like 'Einstellu...' instead of 'Einstellungen', or buttons that overlap neighbouring elements. Critical actions become unreadable or unclickable.
How to fix it
Design and implement flexible layouts that adapt to the content length of all locales.
Replace fixed widths with min-width, max-width and flex layouts. Use CSS Grid or Flexbox to distribute space dynamically.
Enable text wrapping: use overflow-wrap: break-word and avoid white-space: nowrap for translatable content.
Test your UI with pseudo-localization that lengthens all strings by 40% to simulate the worst case — even before sending strings to translators.
Error #5: Date and number formatting
Dates and numbers can seem universal, but 01/02/2025 means January 2 in the US and February 1 in Europe. Commas and periods in numbers change meaning: 1,000.50 (US) vs 1.000,50 (Germany). Getting this wrong leads to confusion, data errors and loss of trust.
['Never format data or numbers manually with string templates — always use Intl APIs', 'Store all data in ISO 8601 and currencies in the smallest unit (cents) internally', 'Test with locales that use different decimal separators, date orders, and calendar systems']
Never format data or numbers manually with string templates—always use Intl APIs
Store all dates in ISO 8601 and currencies in the smallest unit (cents) internally
Test with locales that use different decimal separators, date orders, and calendar systems
A typical scenario
Developers format a date as MM/DD/YYYY and a price as $1,234.50 — correct for US users.
A German user sees the date 03/04/2025 and reads it as 3 April (DD/MM/YYYY convention). The price $1,234.50 looks as if it should be 1.234,50.
Result: The user books a flight for the wrong date and questions the price format. Support tickets rise in the German market by 15%.
Why this is a problem
Date and number formatting affects every data display in your app: tables, charts, forms, invoices, reports. A global fix covers hundreds of instances.
Hard-coded format strings like toLocaleDateString('en-US') or manual formatting with template literals ignore the user's actual locale. Even the right locale but the wrong calendar system (Gregorian vs. Hijri) causes problems.
Users read data incorrectly and enter data in the wrong format. A European user who sees 03/04/2025 might interpret it as 3 April instead of 4 March — missed appointments or incorrect bookings.
How to fix it
Use the built-in Intl API or a locale-aware formatting library for all dates, times, numbers, and currencies.
Replace manual formatting with Intl.DateTimeFormat(locale) for dates and Intl.NumberFormat(locale, { style: 'currency', currency }) for prices.
Store data internally in ISO 8601 format (YYYY-MM-DD) and format it only for display using the user's locale.
Test critical date/number displays with at least 5 different locales: en-US, de-DE, ja-JP, ar-SA and zh-CN to cover the most important formatting variants.
Error #6: Forgotten RTL languages
Right-to-left (RTL) languages like Arabic, Hebrew, and Persian are used by over 500 million people. Yet most apps are designed exclusively for left-to-right (LTR) layouts. RTL support means more than just flipping text — the entire UI must be mirrored.
['From day one, use only logical CSS properties (inline-start/end, block-start/end)', 'Test your app with dir='rtl' on the HTML element at every sprint review', 'Create an RTL testing checklist for navigation, icons, forms, and progress indicators']
From day one, use only logical CSS properties (inline-start/end, block-start/end)
Test your app with dir='rtl' on the HTML element at every sprint review
Create an RTL test checklist for navigation, icons, forms, and progress indicators
A typical scenario
Developers use margin-left: 16px and text-align: left across the entire app — standard LTR practice.
The app starts in Saudi Arabia. Back arrows point forward, sidebars appear on the wrong side, and numeric data are misaligned.
Result: Arabic users leave the app after 30 seconds. The team needs 4 weeks of emergency CSS refactoring to fix the issue.
Why this is a problem
RTL support affects every single component in your app. Introducing RTL later typically requires rewriting 30-50% of all CSS rules and checking every icon and layout.
CSS properties like margin-left, padding-right, text-align: left, and float: left hardcode the direction. Directional icons (arrows, progress indicators) point in the wrong direction. Even border-radius values must be mirrored.
Arabic users see navigation in the wrong corner, progress bars running backwards, and text that collides with UI elements. The app feels foreign and unusable.
How to fix it
Use logical CSS properties and test with RTL layout from the start.
Replace all physical CSS properties with their logical equivalents: margin-left → margin-inline-start, padding-right → padding-inline-end, text-align: left → text-align: start.
Set the dir attribute on your HTML root element based on the active locale. Use the :dir(rtl) CSS pseudo-class for RTL-specific overrides.
Check all icons for directional meaning. Replace direction-bound icons with mirrored versions or use CSS transform: scaleX(-1) for RTL contexts.
Mistake #7: Images with text
Embedding text in images—whether in hero banners, buttons, infographics, or screenshots—is a localization nightmare. Every image with text must be recreated for each language, increasing design costs and delaying releases.
['Never place translatable text directly in raster images (PNG, JPG)', 'Use CSS text overlays on background images for hero banners and CTAs', 'Automate the screenshot generation for App Store listings and marketing pages']
Never embed translatable text directly into raster images (PNG, JPG)
Use CSS text overlays on background images for hero banners and CTAs
Automate screenshot generation for app store listings and marketing pages
A typical scenario
A designer creates a promo banner with the text 'Start Your Free Trial' embedded directly in the image.
The localization team translates all UI strings, but the banner still shows English text on the German page.
Result: The German landing page has an irritating English banner. Creating localized banners for 15 languages takes three days of design work and delays the launch.
Why this is a problem
A typical marketing page has 5-10 images with text. With 15 languages, that's 75-150 image variants that must be created, maintained, and updated with every design change.
Text embedded in images (PNG, JPG, SVG with embedded text) cannot be extracted by translation tools. Each localized version requires a designer to manually edit the source file, export it, and upload it.
Users see images with text in a foreign language, or worse, a mix of translated UI and untranslated images. This looks inconsistent and undermines brand trust.
How to fix it
Separate text from images using CSS overlays, SVGs with translatable text elements, or dynamic image generation.
Use CSS to place translatable text over background images: position the text layer with absolute positioning over the image container.
For infographics or diagrams, use SVGs with <text> elements that reference translation keys, instead of embedding raw strings.
For app screenshots in marketing materials, automate screenshot generation with tools like Fastlane (mobile) or Playwright (web), which capture screenshots in every locale.
Mistake #8: Missing translations not handled
Translations are always incomplete during development. New features add strings faster than translators can translate them. Without proper fallback handling, missing translations cause crashes, empty UI elements, or raw translation keys shown to users.
['Always configure at least one fallback language in your i18n setup', 'Log missing translation keys to your monitoring system for tracking', 'Set a minimum translation coverage level before a locale can go live']
Always configure at least one fallback language in your i18n setup
Log missing translation keys to your monitoring system for tracking
Set a minimum translation coverage before a locale is allowed to go live
A typical scenario
Developers add a new 'Premium Features' section with 15 new translation keys. The English version is shipped immediately.
The French translations are not ready yet. The French page shows raw keys: 'premium.feature1.title', 'premium.feature1.description'.
Result: French users see a broken page full of developer-side key names. The support team receives dozens of bug reports.
Why this is a problem
As your app grows, the gap between English strings and translations in other languages widens. An app with 100 languages and 2,000 strings could have 10,000+ missing translations at any given moment.
Without fallback logic, a missing translation key returns undefined, null, or the raw key string (e.g., 'checkout.confirmButton'). Template engines can throw errors, crash the page, or render nothing.
Users see a broken UI: empty buttons, missing labels, or cryptic strings like 'nav.settings.title' instead of real text. That's confusing and unprofessional.
How to fix it
Configure a robust fallback chain and monitor translation coverage across all locales.
Set up a fallback language chain in your i18n configuration: missing French (fr) keys fall back automatically to English (en).
Add a missing-key handler that logs untranslated keys to your monitoring system (e.g., Sentry, Datadog) without compromising the user experience.
Create a translation-coverage dashboard that tracks completion per locale and blocks releases when coverage falls below a threshold (e.g., 95%).
Error #9: Encoding issues
Encoding issues are the silent killer of localization. Everything looks fine in English and European languages, but as soon as you add Chinese, Japanese, Korean, Arabic or emoji, garbled characters (mojibake) appear. These bugs are notoriously hard to trace.
['Use UTF-8 encoding everywhere — source files, database, API responses and HTML meta tags', 'Use utf8mb4 in MySQL (not utf8), to support the full Unicode range including emoji', 'Test with real content in CJK, Arabic and emoji to catch encoding problems early']
Use UTF-8 encoding everywhere—source files, databases, API responses, and HTML meta tags
Use utf8mb4 in MySQL (not utf8) to support the full Unicode range, including emoji
Test with real content in CJK, Arabic, and emoji to catch encoding issues early
A typical scenario
Developers set up a MySQL database with latin1 collation (the old default). The app source code uses UTF-8.
Japanese users register with their real names. The database stores '田中太郎' as corrupted bytes.
Result: the user profile shows garbled text. Even worse: search and sorting both break for all CJK names, affecting thousands of users.
Why this is a problem
Encoding issues spread across your entire stack. A misconfigured database collation can corrupt millions of records — repairing it may require costly data migrations.
Inconsistent encoding across the stack — UTF-8 in the source files, Latin-1 in the database, and Windows-1252 in the API response — breaks multibyte characters. A single misconfigured layer turns '日本語' into '????' or '日本èª'.
Users see garbled text, question marks, or empty boxes where their language should appear. In the worst case, form submissions are permanently corrupted in the database.
How to fix it
Enforce UTF-8 encoding consistently across every layer of your application stack.
Set all source files to UTF-8 (configure your editor and .editorconfig). Include <meta charset='UTF-8'> in your HTML and 'Content-Type: application/json; charset=utf-8' in API responses.
Configure your database to utf8mb4 (not just utf8, which is a 3-byte subset in MySQL). Set the connection collation to utf8mb4_unicode_ci.
Choose fonts that cover your target scripts: Latin, Cyrillic, CJK, Arabic, Devanagari. Use system font stacks or Google Fonts with language subsets for optimal loading.
Test your i18n implementation
Length extension test
Extend all translated strings by 30-40% to simulate text expansion that occurs in verbose languages like German, Finnish, or Greek. This covers fixed-width containers, truncated labels, and overflowing buttons, before you even start translating. Many pseudo-localization tools include this as a built-in feature.
"Send" → "Ṡééééñðéñ_éxpáñðéð" (40% longer)
Pseudo-localization
Pseudo-localization replaces every character with an accented equivalent (e.g., 'a' → 'á') and wraps strings with markers like [!! and !!]. This immediately shows which strings are hard-coded and which come from the translation system. Run pseudo-localization as part of your CI pipeline to automatically catch regressions.
Any on-screen text that is NOT wrapped in [!! !!]-markers is hard-coded and must be externalized. This test catches 95% of overlooked strings in under a minute.
"Send message" → "[!! Ñáçḥŕíçḥṫ ṡéñðéñ !!]"
RTL layout test
Even without Arabic or Hebrew translations, you can test RTL layouts by adding dir='rtl' to your HTML root element. This immediately reveals directional CSS bugs: icons that are misaligned, margins on the wrong side, broken navigation, and incorrectly ordered flex items. Make this a standard check in every sprint review — it only takes 10 seconds to toggle and catches issues that would otherwise take weeks to fix in production.
The i18n checklist
['All user-visible strings are externalized to resource files', 'Do not use string concatenation to build sentences', 'Plural rules implemented with ICU MessageFormat or an equivalent', 'Date, time, and number formatting use locale-aware APIs', 'RTL layouts tested with Arabic or Hebrew content', 'Flexible UI with no fixed widths to accommodate text expansion', 'Fallback language configured and tested for missing keys', 'UTF-8 encoding consistently across all files and databases', 'App Store and Play Store metadata localized for each market', 'Screenshots and marketing assets updated for each language']