Common Language Switching Bugs and How to Catch Them

Common Language Switching Bugs and How to Catch Them

January 10, 2026 · 16 min read · Common Issues

Common Language Switching Bugs and How to Catch Them

Language switching is a routine operation for multilingual users, yet it remains a fertile source of defects that escape scripted test suites. When an app changes its locale at runtime, any piece of UI, data format, or behavior that assumes a single language can break in subtle ways—misaligned layouts, truncated text, wrong date formats, or even crashes. The impact ranges from mild annoyance to blocked core flows such as login or checkout, especially for users who rely on a non‑default language for accessibility or legal reasons. Detecting these issues before release requires a combination of systematic manual checks, targeted automated tests, and exploratory techniques that mimic real‑world personas.

This guide walks through the most common language‑switching bug patterns, explains why each occurs, shows what the user sees, provides reproducible steps, and offers concrete fixes and preventive measures. You’ll find‑ings are summarized in two reference tables, and a short checklist helps you embed the practices into your CI pipeline. Throughout, we illustrate how persona‑driven autonomous exploration—such as the approach used by SUSA—surfaces issues that deterministic scripts often miss.

---

1. Understanding Language Switching in Software

1.1 Locale, Language, and Region Concepts

A locale is more than a language code; it bundles language (en), script (Latn), territory (US), and sometimes variant (en_US_POSIX). Mobile platforms expose this through Configuration.locale (Android) or Locale.current (iOS), while web apps read navigator.language or the Accept‑Language header. When a user changes the system language or selects a language inside the app, the framework should reload resources, re‑format numbers/dates, and flip layout direction for right‑to‑left (RTL) scripts.

1.2 Typical Flows That Trigger a Switch

Each entry point can hit a different code path, which is why bugs often appear only for certain triggers.

---

2. Bug Pattern #1: Hard‑coded UI Strings

2.1 Why It Happens

Developers sometimes embed literal text directly in layout XML, storyboards, or JSX instead of referencing a resource file. The intent may be rapid prototyping, but the string stays unchanged when the locale switches.

2.2 Symptom

Labels, button titles, or toast messages remain in the source language (often English) while the rest of the UI translates. Users notice a “mixed language” screen, which can be confusing or appear unprofessional.

2.3 Reproduction Steps

  1. Set device language to French (fr‑FR).
  2. Launch the app and navigate to a screen known to contain a hard‑coded string (e.g., a “Submit” button).
  3. Observe that the button still reads “Submit”.

2.4 Fix & Prevention

2.5 Code Example

Android (XML)


<!-- Before -->
<Button
    android:id="@+id/btnSubmit"
    android:text="Submit"
    ... />

<!-- After -->
<Button
    android:id="@+id/btnSubmit"
    android:text="@string/btn_submit"
    ... />

iOS (SwiftUI)


// Before
Button("Submit") { … }

// After
Button(LocalizedStringKey("btn_submit"), action: { … })

Web (React‑i18next)


// Before
<button>Submit</button>

// After
<button>{t('btn_submit')}</button>

---

3. Bug Pattern #2: Missing Fallback Resources

3.1 Why It Happens

When a locale‑specific folder (values-fr, fr.lproj, locales/fr.json) is absent, the framework falls back to the default (values, Base.lproj, locales/en.json). If the default file itself is missing a key, the app may show the raw key or an empty string.

3.2 Symptom

Users see [missing: welcome_title] or blank labels after switching to a language that lacks a translation file. In some cases the app crashes when trying to format a null string.

3.3 Reproduction Steps

  1. Remove the values-fr folder from an Android project (or delete fr.json from the web bundle).
  2. Set device language to French.
  3. Open the app and look for any screen that uses a string only defined in values-en.

3.4 Fix & Prevention

3.5 Code Example (Key‑check script)


#!/usr/bin/env bash
BASE="locales/en.json"
for f in locales/*.json; do
  [[ $f == $BASE ]] && continue
  missing=$(jq -r 'keys[] as $k | select(.[$k] | not) | $k' "$BASE" | \
                grep -vf <(jq -r 'keys[]' "$f"))
  if [[ -n $missing ]]; then
    echo "Missing keys in $f:"; echo "$missing"; exit 1
  fi
done

---

4. Bug Pattern #3: Incorrect Locale Propagation (Activity/ViewController Not Restarted)

4.1 Why It Happens

On Android, changing Configuration.locale does not automatically recreate the current activity unless you handle onConfigurationChanged. On iOS, updating Locale.current does not trigger a view refresh; you must observe NSLocale.currentDidChangeNotification. Forgetting to reload the UI leaves stale strings.

4.2 Symptom

After picking a new language from an in‑app settings screen, the app’s navigation bar or tab titles stay in the previous language, while newly pushed screens show the correct language.

4.3 Reproduction Steps

  1. Open the app in English.
  2. Navigate to Settings → Language → Select Spanish.
  3. Return to the home screen; observe that the toolbar title remains English.

4.4 Fix & Prevention

4.5 Code Example (Android)


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        // Forces reload of resources with the new locale
        recreate()
    }
}

---

5. Bug Pattern #4: Date/Time/Formatting Mismatch After Switch

5.1 Why It Happens

Formats for dates, times, numbers, and currencies are locale‑dependent. If the app caches a SimpleDateFormat, DateFormatter, or Intl.DateTimeFormat object created before the locale change, subsequent calls use the outdated pattern.

5.2 Symptom

A user switches to Japanese and sees dates displayed as MM/dd/yyyy instead of yyyy/MM/dd, or currency shown with a dollar sign rather than ¥. In extreme cases, parsing fails and throws an exception.

5.3 Reproduction Steps

  1. Set device language to German (de‑DE).
  2. Open a screen that shows a timestamp (e.g., “Last updated: 3/5/2024”).
  3. Change language to Japanese (ja‑JP) without restarting the app.
  4. Observe the timestamp still uses the German pattern (03.05.2024) instead of Japanese (2024/03/05).

5.4 Fix & Prevention

5.5 Code Example (Kotlin)


fun formatTimestamp(millis: Long): String {
    // Always use the current locale
    return java.time.Instant.ofEpochMilli(millis)
        .atZone(java.time.ZoneId.systemDefault())
        .format(DateTimeFormatter.ofPattern("MM/dd/yyyy")
            .withLocale(Locale.getDefault()))
}

---

6. Bug Pattern #5: Right‑to‑Left Layout Breakage

6.1 Why It Happens

RTL languages (Arabic, Hebrew, Urdu) require mirroring of horizontal layout constraints. Hard‑coded paddings, absolute pixel values, or missing android:autoMirrored="true" on drawables cause clipped text, overlapping icons, or misaligned icons.

6.2 Symptom

When the user selects Arabic, the action‑bar icon appears on the left side instead of the right, text fields lose their hint padding, and horizontal scroll views scroll in the wrong direction.

6.3 Reproduction Steps

  1. Set device language to Arabic (ar‑SA).
  2. Open a screen containing a Toolbar with a navigation icon and an EditText.
  3. Verify that the navigation icon is mirrored and the EditText hint starts at the right edge.

6.4 Fix & Prevention

6.5 Code Example (Android XML)


<!-- Before -->
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:hint="@string/enter_email" />

<!-- After -->
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:hint="@string/enter_email"
    android:importantForAccessibility="yes" />

---

7. Bug Pattern #6: Input Method / Keyboard Issues

7.1 Why It Happens

Changing the system language often switches the default keyboard layout. If an app manually sets inputType or expects a specific key layout (e.g., numeric PIN pad), the new keyboard may lack those keys or produce unexpected characters.

7.2 Symptom

A user switches to a Hindi keyboard and finds that the “Done” button on the soft keyboard does not close the IME, or numeric fields accept Devanagari digits instead of ASCII numerals, causing validation failures.

7.3 Reproduction Steps

  1. Install a Hindi keyboard (or any non‑Latin layout).
  2. Set device language to Hindi.
  3. Focus on a EditText configured with inputType="numberDecimal".
  4. Attempt to enter a decimal number; observe that the entered text contains Hindi numerals.

7.4 Fix & Prevention

7.5 Code Example (Android InputFilter)


val decimalFilter = InputFilter { source, start, end, dest, dstart, dend ->
    val result = StringBuilder(dest).apply { replace(dstart, dend, source.subSequence(start, end)) }
    if (!result.toString().matches(Regex("\\d*\\.?\\d{0,2}"))) {
        return@InputFilter "" // reject change
    }
    return@InputFilter null
}
editText.filters = arrayOf(decimalFilter)

---

8. Bug Pattern #7: Persistent Preference Overwrites

8.1 Why It Happens

Apps sometimes store the selected language in SharedPreferences (Android) or UserDefaults (iOS) and read it on every launch. If the code writes the preference *after* the system locale has already been applied, the next start may revert to the previously saved value, overriding the user’s system choice.

8.2 Symptom

A user changes the device language to French, launches the app, sees French UI, then exits and relaunches—only to find the UI back in English because the app forced its stored preference.

8.3 Reproduction Steps

  1. Set device language to French.
  2. Open the app; confirm UI is French.
  3. Close the app via recent‑apps swipe.
  4. Reopen the app; observe language reverts to English.

8.4 Fix & Prevention

8.5 Code Example (Android Preference Handling)


fun getAppLocale(context: Context): Locale {
    val prefs = PreferenceManager.getDefaultSharedPreferences(context)
    val langKey = prefs.getString("pref_language", "")
    return if (langKey.isEmpty() || langKey == "system") {
        Locale.getDefault()
    } else {
        Locale.forLanguageTag(langKey)
    }
}

---

9. Bug Pattern #8: Cached Assets / CDN Localization Stale

9.1 Why It Happens

Many apps download localized strings or images from a CDN and cache them locally (e.g., using OkHttp cache or NSURLCache). When the user switches language, the app may still serve the previously cached bundle, leading to outdated translations or missing assets.

9.2 Symptom

After switching to Spanish, certain help images still show English screenshots, or a downloaded JSON file contains English keys despite the Accept-Language: es header.

9.3 Reproduction Steps

  1. Connect to a staging server that serves language‑specific JSON under /i18n/{lang}.json.
  2. Set device language to English, launch the app, and let it download and cache en.json.
  3. Change device language to German, force a network refresh (pull‑to‑refresh or toggle airplane mode).
  4. Observe that the app still displays English strings because it read the stale cache.

9.4 Fix & Prevention

9.5 Code Example (OkHttp Cache Control)


val client = OkHttpClient.Builder()
    .cache(Cache(File(cacheDir, "http-cache"), 10 * 1024 * 1024L))
    .addInterceptor { chain ->
        val request = chain.request()
            .newBuilder()
            .header("Accept-Language", Locale.getDefault().language)
            .build()
        val response = chain.proceed(request)
        // Force revalidation if locale changed
        response.newBuilder()
            .removeHeader("Pragma")
            .header("Cache-Control", "no-cache")
            .build()
    }
    .build()

---

10. Bug Pattern #9: Accessibility Labels Not Updated

10.1 Why It Happens

Accessibility labels (contentDescription, accessibilityLabel, aria-label) are often set once in viewDidLoad or XML and never refreshed when the locale changes. Screen‑reader users then hear outdated or missing descriptions.

10.2 Symptom

A TalkBack user switches to Hindi and hears “button” instead of the translated label “送信ボタン”.

10.3 Reproduction Steps

  1. Enable TalkBack (Android) or VoiceOver (iOS).
  2. Set device language to Hindi.
  3. Navigate to a screen with a button that has a hard‑coded contentDescription.
  4. Listen to the spoken feedback; note that it remains in English.

10.4 Fix & Prevention

10.5 Code Example (iOS)


override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    sendButton.accessibilityLabel = NSLocalizedString("send_button_label", comment: "")
}

---

11. Bug Pattern #10: Security / Localization Bypass

11.1 Why It Happens

Some apps accept a language override via query parameter (?lang=fr) or HTTP header and directly use it to load resources without validation. An attacker can inject unexpected values (e.g., ../../etc/passwd) leading to path traversal or loading of arbitrary files.

11.2 Symptom

A malicious user crafts a link that forces the app to load a locale file from the filesystem, potentially exposing secrets or causing a crash when the file is not a valid JSON.

11.3 Reproduction Steps

  1. Find an endpoint that loads locales/{lang}.json based on a user‑supplied lang parameter.
  2. Request https://example.com/api/config?lang=../../etc/passwd.
  3. Observe the app attempts to parse the passwd file, resulting in an error screen or crash.

11.4 Fix & Prevention

11.5 Code Example (Java Whitelist)


private static final Set<String> ALLOWED = Set.of(
    "en", "fr", "de", "es", "ja", "zh", "ar"
);

public Locale resolveLocale(String tag) {
    if (tag == null || !ALLOWED.contains(tag.toLowerCase(Locale.ROOT))) {
        throw new IllegalArgumentException("Unsupported language: " + tag);
    }
    return Locale.forLanguageTag(tag);
}

---

12. Bug Pattern #11: Cross‑platform Inconsistency (Mobile vs Web)

12.1 Why It Happens

Teams often maintain separate i18n files for native apps and the web portal. When a new feature is added, the translation may be added to Android strings.xml and iOS Localizable.strings but forgotten in the web messages.json. Conversely, the web team may adopt a different naming convention (submitBtn vs btn_submit).

12.2 Symptom

A user switches language on the web and sees the new feature’s button label in English, while the same button in the mobile app shows the correct translation.

12.3 Reproduction Steps

  1. Add a new string key welcome_promo with value in all mobile locale files.
  2. Forget to add it to the web messages.json.
  3. Change language to Spanish on the web portal and navigate to the promo page.
  4. Observe the label appears as welcome_promo.

12.4 Fix & Prevention

12.5 Code Example (Key‑set comparison script)


import json, os, glob

def load_keys(path):
    with open(path, encoding='utf-8') as f:
        data = json.load(f)
    return set(_flatten(data.keys()))

def _flatten(keys):
    for k in keys:
        if isinstance(k, dict):
            yield from _flatten(k.keys())
        else:
            yield k

web_keys = load_keys('web/locales/en.json')
android_keys = load_keys('android/app/src/main/res/values/strings.xml')
# … similarly for iOS

missing_in_web = android_keys - web_keys
if missing_in_web:
    raise SystemExit(f"Web missing keys: {missing_in_web}")

---

13. Manual Testing Checklist for Language Switching

StepActionPersonaExpected Result
1Change system language to a non‑default locale (e.g., Japanese)Curious noviceApp restarts or foregrounds with all UI in Japanese
2Open in‑app language picker and select a different language (e.g., Arabic)Power userUI updates instantly; layout mirrors correctly
3Rotate device while in a language‑specific screenElderlyNo loss of text; TalkBack/VoiceOver reads correct labels
4Enter data in a form using the native keyboard for the selected languageAccessibility userInput accepted, validation works, keyboard hides on done
5Background the app, change system language again, resumeImpatient userUI reflects the new language without restart
6Navigate to a deep link containing ?lang=frAdventurous userApp opens and displays French UI, no errors
7Take a screenshot after each switch and compare to baselineQA leadNo missing strings, no overlapped layouts, no truncated text
8Verify that dates, times, numbers, and currencies follow locale conventionsData‑driven userFormats match CLDR expectations for the locale

Perform this matrix on every release candidate; any deviation flags a language‑switching defect.

---

14. Automated Detection Strategies

14.1 Unit & Integration Tests

14.2 UI Tests with Explicit Locale Switches

These tests should be part of the nightly suite; they catch regressions that unit tests miss because they exercise the full resource‑loading pipeline.

14.3 Pseudo‑localization & Visual Diff

14.4 CI Gate Example (GitHub Actions)


name: i18n Checks
on: [push, pull_request]
jobs:
  locale:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Pseudo-locale Android build
        run: ./gradlew assemblePseudoDebug
      - name: Run screenshot tests
        run: ./gradlew connectedPseudoDebugAndroidTest
      - name: Web i18n lint
        run: npm run lint:i18n   # custom script that checks key coverage
      - name: Playwright language switch
        run: npx playwright test --project=chromium --grep "@language-switch"

---

15. Persona‑Driven Autonomous Exploration (SUSA)

SUSA explores an application without pre‑written scripts by simulating distinct user personalities. Each persona has a configured behavior model:

PersonaTraitsRelevance to Language Switching
CuriousTaps every visible element, opens menus, tries settingsLikely to discover the in‑app language picker and change locale mid‑session
ImpatientPerforms actions quickly, often backgrounding the appTriggers resume‑after‑language‑change scenarios that expose stale UI
NoviceFollows on‑boarding hints, rarely uses system settingsMay rely solely on in‑app picker, highlighting bugs where picker does not propagate
ElderlyUses larger fonts, enables accessibility servicesChecks that accessibility labels update with locale
AccessibilityActivates TalkBack/VoiceOver, uses alternative input methodsVerifies that input methods and accessibility labels stay correct
Power userUses deep links, shortcuts, and rapid setting togglesExercises intent‑based language overrides and deep‑link language params
AdversarialAttempts malformed inputs, injects unexpected valuesTests language‑parameter validation and guards against injection
.........

During a run, SUSA records every screen visited, every input supplied, and any exception or ANR. When it detects a language change (either via system setting or in‑app toggle), it automatically:

  1. Captures screenshots before and after the switch.
  2. Compares text layers using OCR to spot missing translations.
  3. Checks layout direction via UI hierarchy attributes.
  4. Logs any crash, ANR, or accessibility warning.

Because SUSA does not rely on predetermined navigation paths, it reaches edge cases such as:

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free