Key takeaways
- Localization testing is not translation checking. It's verifying that your app renders correctly, functions correctly, and doesn't crash across every locale, script, calendar system, number format, and layout direction your users actually use.
- The bugs that ship aren't translation quality ones (users forgive awkward phrasing). They're layout breaks, pluralization crashes, and date format bugs that produce broken UI or wrong data. These are what testing needs to catch.
- Pseudo-localization catches 80% of localization bugs without any actual translation. Teams that skip it wait for real translations, ship untested code, and then fix same bugs 15 times across 15 languages.
Localization testing verifies that your app works correctly across every locale it supports: languages, scripts, layout directions, date formats, number formats, calendar systems, and cultural conventions. Every generic post lists "RTL support" and "date formats" as if those are whole scope. They're surface.
International releases push localization testing higher up mobile testing stack than most teams expect, because one supported country adds dozens of test dimensions and each one has its own failure modes. This post goes into what actually breaks in production and how to catch it before it ships.
Internationalization vs localization vs globalization
These three terms get mixed up constantly. They're different concepts and localization testing depends on all three being handled correctly.
- Internationalization (i18n) happens in code. It's engineering work that prepares your app to support multiple locales: extracting strings, using locale-aware formatting APIs, supporting variable text direction, handling variable date/number formats.
- Localization (l10n) happens after i18n. It's process of adapting app for a specific locale: translating strings, adapting images, adjusting cultural references, formatting dates and numbers per that locale.
- Globalization (g11n) is umbrella. Company-level strategy for entering global markets.
You can't test l10n if i18n isn't done. If strings are hardcoded, no amount of translation catches that. If dates are formatted as MM/DD/YYYY in code, changing locale won't help.
What localisation testing verifies:
- Translations are correct and contextually appropriate
- Text renders correctly in all supported scripts
- Layout adapts to text expansion and RTL direction
- Numbers, dates, currencies, addresses format per locale
- Pluralization rules apply correctly
- Cultural elements (colors, icons, imagery) are appropriate
- App doesn't crash on any supported locale
Once functional coverage is stable, localization testing is usually the next expansion area. Device fragmentation and language fragmentation overlap more than most plans admit, and both feed into localization testing as matrix scales.
Character encoding and text handling
This is where the deepest bugs live and where SERP is thinnest.
UTF-8 is not enough
Most modern mobile apps use UTF-8 for string encoding. That handles basic case. It doesn't handle:
- Surrogate pairs: characters outside the Basic Multilingual Plane use two 16-bit code units in UTF-16 and 4 bytes in UTF-8. Emoji, ancient scripts, mathematical symbols. Old string handling code assumes one character = one code unit and breaks.
- Combining characters: accented characters can be represented as either precomposed (é as one code point) or decomposed (e + combining acute). Both look the same. Comparison, search, and character counting all behave differently.
- Zero-width joiners (ZWJ): modern emoji sequences like family emoji or skin tone modifiers use invisible ZWJ characters. String truncation that cuts a ZWJ sequence produces broken emoji.
- Bidirectional control characters: invisible characters that change text direction can be exploited (Trojan Source attack) or accidentally introduced in translation.
Test cases that surface encoding bugs:
- Enter a name with combining diacritics (é as one character vs e + combining acute)
- Enter emoji with ZWJ (👨👩👧👦 family emoji)
- Enter text with mixed scripts (Latin + CJK + emoji)
- Enter text at exactly character limit and one under
- Enter text with right-to-left override characters
- Copy-paste text from various sources (Word, browser, other apps)
The Unicode CLDR (Common Locale Data Repository) is authoritative source for locale data on both iOS and Android. Every locale-aware formatting decision ultimately comes from CLDR. Worth understanding when you're building tests that verify format correctness.
Right-to-left layout and bidirectional text
RTL support means more than mirroring layout.
What RTL actually requires:
- Layout mirroring: entire UI flips horizontally. Navigation drawers slide from right. Back arrows point right (or become forward arrows).
- Text alignment: paragraph text aligns right; numbers still align left in some contexts (currency amounts).
- Bidirectional text: RTL text with embedded LTR content (English brand names, URLs, phone numbers) requires Unicode Bidirectional Algorithm to handle correctly.
- Mixed content: mixing Arabic and English in the same sentence or displaying an Arabic name next to an English timestamp.
Common RTL bugs:
- Icons that should mirror don't (back arrow doesn't flip)
- Icons that shouldn't mirror do (a clock icon flipped looks broken)
- Chevrons in list rows point wrong way
- Progress bars fill left-to-right when they should fill right-to-left
- Sliders drag in wrong direction
- Copy operations preserve LTR order and produce garbled text
- Number sequences within RTL text render as isolated LTR blocks incorrectly
Test cases for RTL:
- Switch device language to Arabic or Hebrew and walk through every major flow
- Test text input with mixed Arabic and English
- Test text truncation on RTL languages
- Test forms with Arabic labels and English input
- Test copy-paste between RTL and LTR contexts
- Test share sheets containing bidirectional content
On iOS, use NSLocale and UIView.semanticContentAttribute to test layout direction. On Android, use LayoutDirection and RTL support via android:supportsRtl="true" in manifest.
Text expansion coefficients per language
Localized text isn't same length as the source. This is single most common cause of layout breaks in localisation.
Approximate expansion rates from English:
- Chinese (Simplified): 20-30% shorter (character count), but wider glyphs
- Japanese: similar to Chinese, but with mixed scripts
- Korean: 15-25% shorter
- Spanish, Portuguese, Italian: 15-25% longer
- French: 20-30% longer
- German: 30-40% longer
- Russian: 30-40% longer
- Finnish, Turkish: 40-50% longer (agglutinative languages produce long compound words)
- Arabic: similar length but different visual weight
- Vietnamese: similar length but taller glyphs due to diacritics
What these coefficients mean in practice:
- A button that says "OK" in English fits fine. In German it might be "Bestätigen" (10 characters). Button truncates or wraps.
- A tab bar with 5 tabs works in English. In Finnish, labels won't fit. Layout breaks.
- A modal title that fits in one line in English wraps to three lines in Russian. Below-the-fold content pushes further down.
Test cases:
- Layout every screen in longest expected language for that screen
- Check for truncation, ellipsis, wrapping, overflow
- Test with shortest expected language too (extra whitespace looks wrong)
- Test with mixed languages where user-generated content is in one language and UI is in another
The design principle: design for 40% expansion. If your UI works in a 40%-expanded pseudo-translation, it works in almost every real language.
Pluralization rules
English has two plural forms: singular (1) and plural (everything else). Most languages have more. Getting this wrong produces sentences like "You have 1 items" or crashes when plural form is missing.
Plural forms by language family:
- English, German, Dutch, Spanish: 2 forms (one, other)
- French, Portuguese: 2 forms but 0 uses of the singular
- Russian, Ukrainian, Polish: 4 forms (one, few, many, other) – the "few" form applies to 2-4, "many" to 5+
- Arabic: 6 forms (zero, one, two, few, many, other)
- Chinese, Japanese, Korean, Vietnamese: 1 form (no plural inflection)
- Latvian: 3 forms including a specific "zero" form
- Welsh: 6 forms
The technical implementation:
- iOS: use NSLocalizedStringKey with plural variants defined in .stringsdict files
- Android: use getQuantityString() with plurals resources in XML
- Cross-platform: use ICU MessageFormat syntax, which handles all CLDR plural rules
Test cases for pluralisation:
- Test each plural boundary: 0, 1, 2, 3, 4, 5, 11, 21, 100, 101, 1000
- Test in Russian specifically (4-form language covers most edge cases)
- Test negative numbers if your app displays them
- Test decimals if your app can display "1.5 items"
Teams that hardcode "if count == 1 use singular, else use plural" ship broken pluralisation in every non-English language. Which is almost every language.
Date, time, number, and currency formats
Locale-aware formatting has more edge cases than most posts cover.
Date formats vary in three ways:
- Order: MM/DD/YYYY (US), DD/MM/YYYY (UK, most of Europe), YYYY-MM-DD (ISO 8601, Japan, Korea), YYYY/MM/DD (China)
- Calendar system: Gregorian (most), Buddhist (Thailand), Hijri (Islamic countries), Japanese (Reiwa era), Chinese lunar calendar
- Separators: /, -, ., space
Time formats:
- 12-hour with AM/PM vs 24-hour
- Colon vs period as separator (10:30 vs 10.30)
- Leading zero conventions
- Time zone display
Number formats:
- Thousands separator: comma (US), period (Germany), space (France, Sweden), apostrophe (Switzerland)
- Decimal separator: period (US) or comma (most of Europe)
- Indian numbering: 1,00,000 (one lakh) instead of 100,000
- Chinese numbering: 万 (10,000) as a base unit
Currency:
- Symbol position: $100 (US), 100€ (Europe), €100 (some European locales), R$100 (Brazil), NT$100 (Taiwan)
- Spacing: some locales require a non-breaking space between amount and currency
- Symbol vs code: '$' vs 'USD' vs '$USD'
- Negative amounts: -$100, ($100), $-100, $100
Test cases:
- Display same date in every supported locale
- Display same amount in every supported currency with correct positioning
- Enter dates in every locale-appropriate format
- Test dates around DST transitions in every supported time zone
Address, phone, and name formats
Product forms often assume address formats work everywhere. They don't.
Address format assumptions that fail:
- Street number before street name (US) vs after (Europe)
- ZIP code position and format (5 digits US, 4 digits UK, alphanumeric Canada, none in some countries)
- State/province required (US, Canada) vs not applicable (most countries)
- City-state-country vs "PO Box" as primary line
- Multi-line addresses (Japan) vs single-line
Phone number formats:
- E.164 international format: +[country][number]
- Local formats vary widely: (415) 555-1212 (US), 0207 946 0958 (UK), +33 1 42 34 56 78 (France)
- Some countries have area codes; others don't
- Length varies: US is always 10 digits; others range from 6 to 15
Name formats:
- First name / last name is not universal
- Chinese, Japanese, Korean, Vietnamese: family name first
- Icelandic: patronymic system, no fixed family name
- Spanish/Portuguese: two surnames (father + mother)
- Some cultures: single name
- Some cultures: honorifics that aren't optional
Requiring "first name" and "last name" as separate fields breaks for a large portion of global userbase. The pattern that works: single "full name" field with optional preferred name / display name fields.
Font rendering and script support
Not every font contains glyphs for every script. Testing needs to catch this.
Scripts that require specific font support:
- Bengali, Tamil, Telugu, Malayalam, Kannada (Indic scripts)
- Khmer, Lao, Thai (Southeast Asian scripts)
- Amharic, Tigrinya (Ethiopic script)
- Georgian, Armenian (Caucasian scripts)
- Ancient scripts (rarely needed but real for some apps)
The tofu problem:
When a font doesn't have a glyph for a character, most systems render a "tofu" (a rectangular placeholder). Users see boxes instead of text. Your app looks broken.
iOS handles most scripts via system fonts. Android depends on which OEM shipped device. A Xiaomi phone in Kenya might not have Amharic support even though Ethiopian users are your target market.
Test cases for font rendering:
- Test every supported language on actual device models your users use
- Look for tofu (boxes) in any rendered text
- Test with custom fonts that might not cover all scripts
- Test text weight and style variants (bold and italic) in each script
Fallback font selection is complex. On iOS you can specify a font family, and iOS picks glyphs from fallbacks. On Android it's more OEM-dependent. Some scripts require explicit font bundling in app.
Pseudo-localization: a localisation testing technique that catches most bugs pre-translation
If you skip everything else in this post, don't skip this.
Pseudo-localisation replaces your English strings with modified versions that:
- Add characters that expand length by 30-40%
- Wrap string in brackets so you can spot it visually
- Include accented characters and non-Latin characters
- Preserve meaning enough that you can test flow
Example:
- Original: Welcome to app
- Pseudo-localized: [!!! Ẃëļçömé țö åpp !!!]
Now you run your entire app in pseudo-localization mode. You catch:
- Hardcoded strings that don't get replaced (they show up as English while everything else has brackets)
- Text truncation (expansion revealed layout breaks)
- Character encoding issues (accented characters render or don't)
- RTL issues if you use RTL pseudo-locale
The value:
Pseudo-localization catches roughly 80% of localization bugs before any translator sees strings. Teams that don't use it wait for real translations, ship untested code, then fix same layout bugs 15 times across 15 languages.
Implementation:
- iOS: use "Double-Length Pseudolanguage" and "Accented Pseudolanguage" in Xcode scheme run options
- Android: use "en-XA" (accented English pseudo-locale) and "ar-XB" (bidirectional pseudo-locale) built into Android's aapt2 tooling
- Cross-platform: pseudo-localization tools from Crowdin, Lokalise, POEditor, or homegrown scripts
Pseudo-localization is usually first thing teams should turn on when localization becomes part of roadmap.
Mobile-specific localization pipelines
iOS and Android handle localization differently. Both have their own gotchas.
iOS localization:
- .strings files per language in .lproj directories
- .stringsdict files for plural handling
- Info.plist needs InfoPlist.strings for localized app name and permission descriptions
- Storyboard/XIB localization vs code-based (SwiftUI, UIKit)
- Base internationalization for reducing duplication
- Bundle detection for language override at runtime
Android localization:
- strings.xml per language in res/values-<locale>/ directories
- plurals element for plural handling
- arrays for localized string arrays
- Density-independent icons per locale in some cases
- Resource qualifier system: values-fr-rCA for Canadian French
- Configuration changes trigger re-inflation when locale changes
Common pipeline problems:
- Missing translations render as default language, not as blanks (subtle bug)
- Extra translations for strings that no longer exist waste translator budget
- Contextual mistranslations because translators don't see UI
- Placeholder mismatches (%1$s in English becomes %s in translation, crash)
- HTML entities in strings get double-encoded or unescaped incorrectly
Missing translations that render as default language look OK to most testers, which is why they slip through code review. Contextual mistranslations are other silent killer since translator often sees a spreadsheet of strings with no UI.
Screenshot testing at scale
Localized apps produce a screenshot explosion. One key screen × 40 supported languages × 3 device sizes × 2 orientations = 240 screenshots per screen.
Manual approach:
- Take a golden reference in English
- Take same screenshot in each locale
- Compare visually
- 240 screenshots per screen × 30 screens = 7,200 screenshots to review
This doesn't scale. Teams that try it stop after 2 releases.
Automated approach:
- Screenshot tests generate images on every build
- Diff tools flag layout changes vs golden reference
- Human review only when diff exceeds a threshold
- Tools: Percy, Applitools, native iOS Snapshot Testing, Roborazzi (Android)
What screenshot tests catch:
- Text truncation
- Layout shifts due to text expansion
- RTL rendering issues
- Font substitution (tofu, wrong font selected)
- Overlapping elements
What they don't catch:
- Translation quality
- Cultural inappropriateness
- Semantic correctness
- Functional bugs downstream of localized flows
Screenshot testing is necessary and insufficient. It catches layout bugs. Human review catches content bugs. Both are needed.
In-country vs remote localization testing
Some localization bugs only surface in target market.
What remote testing catches:
- Layout and truncation
- Format correctness
- Character encoding
- Font rendering (mostly)
- Basic translation quality
What only in-country testing catches:
- Cultural inappropriateness (colors, imagery, references that don't translate)
- Local payment methods that only work in-country
- Local phone/address validation that requires local test data
- Content moderation for local sensitivities
- Local network conditions (some countries throttle certain content)
- Store-specific issues (Chinese app stores have different requirements)
The trade-off:
- In-country testing is expensive: local testers, local devices, local network access
- Remote testing covers 80% of bugs at 20% of cost
- The other 20% is often high-impact bugs (payment failure, offensive imagery, government blocks)
Most teams do remote for tier 2/3 markets and in-country for tier 1 markets (top 5-10 markets by revenue). Tools like BrowserStack, LambdaTest, Kobiton let you access devices in specific countries via cloud infrastructure.
Where automated localization testing fits
Selector-based test frameworks (Appium, XCUITest, Espresso) have a specific problem with localization testing: they often assert on visible text, which changes per locale.
The pattern that breaks:
- Test asserts on "Add to cart" visible in English
- Same test in French expects "Ajouter au panier"
- Same test in German expects "In den Warenkorb"
- QA lead maintains 40 language variants of every text assertion
Teams that try to run their functional test suite across all supported locales end up with 40x test maintenance burden. Most give up and only run functional tests in English.
What works:
Vision AI can adapt to visible text without hardcoding. Drizz tests use plain English descriptions of what user does:
- Tap primary button to add items to cart
- Validate confirmation screen shows
The Vision AI reads screen and finds button that matches semantic description, regardless of what text is on it. The same test file works across all supported locales without per-language variants.
Before, on Appium (localization suite):
- 200 functional tests
- Supporting 15 locales
- Full localized suite: 200 × 15 = 3,000 test variants to maintain
- QA team maintains English only, ships regressions in other locales
- Bugs found post-release by users
After, on Drizz (localization suite):
- 200 functional tests
- Same 15 locales supported
- Tests written once, run across all locales
- Screenshot verification catches layout issues per locale
- Bugs caught pre-release
For localization testing at scale, framework choice is difference between "we tested English properly and hope others work" and "we actually verified all 15 locales before ship." The layout bugs, format issues, and functional regressions in non-English locales get caught same way they get caught in English.
Localization testing is what turns a mobile app from a US app into a global one. Getting it right takes both technique and framework, but payoff is a product that works for every user in every market, not just ones you tested manually.
FAQ
What is localization testing?
Verifying an app works correctly across every supported locale: language, script, layout direction, date format, number format, and cultural conventions.
What's difference between i18n and l10n?
Internationalization (i18n) is engineering work supporting locales. Localization (l10n) is adapting app for a specific locale.
What is pseudo-localization?
Modifying English strings with accented characters and length expansion to catch layout and encoding bugs before real translation happens.
Which languages need most testing effort?
RTL languages (Arabic, Hebrew), Russian (4 plural forms), Finnish (long compound words), and CJK languages (font rendering).
Should you test every supported language?
Yes for pseudo-localization and screenshot testing. Real language testing prioritizes top-revenue markets and RTL languages.
What tools help with mobile localization testing?
Crowdin, Lokalise, POEditor for string management. Percy, Applitools for screenshot diff. Xcode and Android Studio for platform-specific testing.


