Back to blog
    August 23, 2026Tutorial

    Testing Twelve Languages Without Twelve Devices

    Localization bugs have a signature: they're found by users, not by QA, and they're found in the languages nobody on the team speaks.

    The reason is procedural rather than technical. Checking one screen in one extra language means opening Settings, changing the system language, navigating a settings app you can no longer read, relaunching the app, finding your way back to the screen, and then reversing all of it. Nobody does that twelve times. So the German build ships with a button that says Zahlungsm... and the Arabic build ships with a back arrow pointing the wrong way.

    The fix isn't more discipline. It's removing the twenty-step loop.

    What actually breaks, in rough order of frequency

    Text expansion. German runs roughly 30% longer than English on average, and short strings are much worse than that — a 5–10 character label can double or triple. "Save" is Speichern. "Settings" is Einstellungen. Finnish, Russian and Polish are comparably unkind. Your button was sized for the English string and it now truncates, wraps into two lines that clip, or pushes its neighbour off screen.

    The screens that break are always the same ones: tab bars, segmented controls, buttons in a fixed-width row, table cells with a label and a value on one line, and anything in a navigation bar.

    RTL layout. Arabic, Hebrew, Persian and Urdu mirror the entire layout — not just text alignment. Back arrows point right. Progress bars fill right to left. Chevrons flip. Drawers open from the other side. Icons that imply direction need mirroring; icons that don't (a play button, a logo) must not be mirrored. Hardcoded left/right margins instead of leading/trailing are where this goes wrong, and they're invisible until you actually look at the RTL build.

    Plural rules. English has two forms. Russian has three, Arabic has six, and plenty of languages have one. Any string built by concatenating a number with a hardcoded suffix is broken in most of the world — "$count items" cannot be localized correctly no matter how good the translator is. This is one of the few localization bugs that's genuinely a code bug rather than a layout bug, and it needs plural-aware string resources, not a longer translation file.

    Formats. Decimal separators (1,234.56 vs 1.234,56), date order, 12- vs 24-hour clocks, first day of the week, currency symbol placement. The bug that actually hurts here is a parsing one: an app that formats correctly for display but parses user input with a fixed locale will reject perfectly valid amounts typed by users in half of Europe.

    Locale-sensitive string operations. The famous one is Turkish: uppercasing "i" in a Turkish locale produces "İ", not "I". Any code that upper- or lower-cases a string for comparison — a header name, a scheme check, a cached key — behaves differently on a Turkish device. It's the archetypal bug that's impossible to find by reading code and trivial to find by running the app in tr-TR.

    Text rendering. Thai and Khmer have no spaces between words and wrap by rules your layout may not implement. Japanese and Chinese break lines mid-"word" by design. Diacritics in Vietnamese and Czech get clipped by line heights that were tuned for English. Devanagari combines glyphs in ways that make character-count-based truncation produce nonsense.

    Untranslated strings. The dull one, and the most common in absolute terms. A string added late, hardcoded in a layout file, or shipped before the translation came back. Every release introduces a few.

    Pseudo-locales: find most of it before translation exists

    You do not need finished translations to find the layout half of this list. Both platforms ship pseudo-locales for exactly this purpose, and they're the highest-value thing in this article relative to effort.

    Android has en-XA — English with accents and padding, e.g. [Ŝéţţîñĝŝ one two] — and ar-XB, a right-to-left pseudo-locale that mirrors your layout while keeping text readable. Both are enabled in developer options on Android 7+.

    iOS offers the equivalent as scheme options in Xcode: Double-Length Pseudolanguage, Accented Pseudolanguage, and Right-to-Left Pseudolanguage.

    Three things fall out of running your app in these:

    1. Anything not in accented text is a hardcoded, untranslated string. This is by far the fastest way to find them — no translator, no diff, just look for the plain English.
    2. Anything truncated or overlapping under double-length will break in German.
    3. The RTL pseudo-locale exposes every hardcoded left/right in your layout before any Arabic exists.

    Run these two on every build. They cost nothing and catch most layout regressions the week they land, rather than the week before a market launch.

    Then run the real languages on real hardware

    Pseudo-locales don't cover rendering, fonts, or input. For that you need the actual locale on an actual device, which is where the twenty-step loop comes back.

    The way out is launching the app directly in a target language instead of reconfiguring the device around it. On RobotActions that's a single action — start the app under test in a chosen language, take your screenshots, move to the next one — so a twelve-language sweep of one screen is twelve launches rather than twelve trips through a Settings app you can't read. Device language can be set outright too when you need the full system context (system dialogs, permission prompts and share sheets are rendered by the OS in the device's language, not your app's).

    A sweep worth automating, roughly:

    js
    const LOCALES = ["de-DE", "fr-FR", "ar-EG", "ja-JP", "tr-TR", "ru-RU", "fi-FI"];
    
    for (const locale of LOCALES) {
      await launchAppInLanguage(locale);
      for (const screen of CRITICAL_SCREENS) {
        await navigateTo(screen);
        await screenshot(`${screen}-${locale}.png`);
      }
    }

    Then diff those screenshots against the previous release rather than reading all 84 by eye. A visual comparison flags the ones that changed; a human looks only at those. Without the diff step this becomes a task nobody repeats after the first time.

    Pick the locale set deliberately. Seven well-chosen locales cover nearly all the failure modes:

    LocaleWhat it's covering
    de-DEText expansion, compound words
    fi-FI or ru-RUWorse expansion, plus non-Latin (Russian)
    ar-EGRTL mirroring, Arabic plural forms, Arabic-Indic digits
    ja-JPCJK line breaking, font fallback, no word spaces
    tr-TRLocale-sensitive casing bugs
    fr-FRPunctuation spacing, formats — and your largest small-expansion market
    en-XAHardcoded strings

    What to assert, not just eyeball

    Screenshots catch layout. A few things are worth asserting programmatically because they're cheap and unambiguous:

    • No visible ellipsis in buttons or tab bars. Truncation in a label is sometimes fine; truncation in a control is almost never fine.
    • No text overflowing its container bounds. The element tree gives you both rectangles.
    • Every user-visible string differs from the English build (excluding proper nouns and known-identical terms). That's the automated version of "look for the plain English", and it's the same trick that catches half-translated releases.
    • Formatted numbers and dates parse back correctly in that locale. A round-trip assertion catches the input-parsing bug that display-only testing never will.

    The part that's a process problem

    Most localization bugs are introduced by a string added on the Tuesday before a release, when translation takes a week. That's not a testing failure, and no amount of device automation fixes it.

    Two habits do:

    • No hardcoded user-visible strings, enforced in review or lint. A hardcoded string is untranslatable by definition, and the pseudo-locale pass makes them trivially visible if one slips through.
    • Design against the long string, not the English one. If the mockup only ever shows Save, the button will be sized for Save. Reviewing designs with the German string in place costs nothing at design time and saves a layout fix at release time.

    Launch your app in any language on a real device →

    Ready to test on real devices?

    Sign in with Google or GitHub and get real iOS and Android devices in your browser — free to try.

    👋 Hi! Need help? Chat with us!

    Chat with us

    Online

    Before we start

    Share your details so we can follow up with you.