Skip to main content

Prevent Android App Bundles From Dropping Language Resources

2026-09-17

Prevent Android App Bundles From Dropping Language Resources

Android App Bundle localization can pass every debug test and then fail after Google Play delivery. The user selects French, the preference changes, but the release app stays in English. The installed APK set has no French resources. A universal APK hid that problem because it packaged every locale.

Another call to setApplicationLocales() will not fix missing files. Locale selection and language resource delivery are separate systems. Choose a delivery policy, match it to the languages in your picker, then test the signed bundle the way Play installs it.

Why the release build behaves differently

An Android App Bundle is a publishing artifact. Users run a set of APKs that Google Play creates for their device. The set can contain a base APK and configuration APKs for ABI, screen density, and language.

Android's guide to configuring the base module states that language configuration APKs are enabled by default. Play initially chooses language resources from the device language settings. If the system language later changes to another language supported by the app, the device can request the additional language configuration APK.

An in-app language picker takes a different path. It can select a language that was absent from the device configuration used to build the installed APK set. The saved locale is valid, yet the corresponding values-fr, values-ja, or other resource directory may not be on the device.

The result often looks like this:

  • Locale state is persisted correctly.
  • Android settings show the selected per-app language.
  • A debug APK or universal APK switches correctly.
  • The signed, device-specific release stays in the default language.
  • Restarting the process does not help.

A developer reproduced that pattern in Google's sample repository: per-app languages worked during development but failed in release bundles. The report covered emulators, a physical device, several SDK levels, and multiple Android Gradle Plugin versions. Treat it as a practitioner report, not a platform guarantee, but use the reproduction shape in your own tests.

Separate the three locale contracts

Debugging gets easier once you separate three contracts that are often treated as one feature.

The supported-locale contract is the exact set the product promises, such as en, fr, de, and ja. Generate that set from one source of truth. Translation files, the picker, LocaleConfig, release checks, and store metadata should not carry independent lists.

The selection contract names the component that owns the active locale. On current Android versions, system per-app language settings and the public locale APIs should remain consistent with any picker inside the app. Android's per-app language preferences guide documents automatic LocaleConfig generation, AndroidX compatibility, and migration from an older custom picker. That migration has its own pitfalls, covered in migrating an Android language picker to per-app language preferences.

The delivery contract answers a separate question: does every install receive every supported language, or does the app fetch missing language resources when the user asks for them? LocaleConfig advertises and selects supported locales. It does not prove that the translated resources were installed.

A release-only failure follows naturally when selection works but delivery was never tested.

Choose a language delivery strategy

Your choice is to package all selectable languages or fetch missing ones on demand. Leaving the App Bundle default in place without choosing either behavior is what creates the gap.

Package every app-selectable language

For an app with a modest number of translated strings and a small locale set, package all language resources in each relevant APK. Android documents disabling language configuration splitting for apps whose independent picker may request resources outside the device language set.

In the app module, make the bundle choice explicit:

android {
    bundle {
        language {
            enableSplit = false
        }
    }
}

All translations are local, so applying French does not depend on another download. The tradeoff is a larger install because every user receives all packaged locales.

Measure the release artifact before using this setting across a large project. Locale count, localized media, and feature-module resources can make the size increase meaningful. Record the compressed download size beside the operational cost of downloads, the offline requirement, and the number of languages users can select independently of system settings.

Download a missing language before applying it

Apps with many locales or large language-specific resources may keep language splits and add an explicit download path. The base-module guidance directs apps with independent language pickers to on-demand language downloads.

Model the picker as a state machine rather than applying a locale immediately:

onLanguageSelected(locale):
  if resourcesAreInstalled(locale):
    applyLocale(locale)
    restartOrRecreateAffectedUi()
  else:
    showDownloading(locale)
    requestLanguageResources(locale)
    if downloadSucceededAndResourcesVerify(locale):
      applyLocale(locale)
      restartOrRecreateAffectedUi()
    else:
      keepCurrentLocale()
      showRetryOrOfflineMessage()

Order matters here: download the resource, verify a lookup, and only then apply the locale. If you persist the request too early, the next launch can restore a language that the installed APK set cannot render. Keep the last working locale until activation succeeds.

On-demand delivery adds product questions that the packaging flag avoids:

  • What does the picker show while a language downloads?
  • Can the user cancel?
  • What happens offline?
  • Is a partial download discarded safely?
  • Does a process restart resume or retry the request?
  • How are obsolete language packs handled after an app update?
  • Do dynamic feature modules need matching language resources too?

If the team cannot own that state machine and its failure handling, packaging all selectable languages is usually the safer release choice.

Match the locale inventory to the artifact

Finding a resource directory in Git tells you nothing about the APK set on a user's phone. Generate or validate one normalized locale inventory before building.

For each supported locale, check:

  1. The locale appears in the product's source-of-truth list.
  2. The app has the expected values resources or generated equivalent.
  3. The in-app picker exposes it only when the release delivery policy supports it.
  4. Generated LocaleConfig output includes it where required.
  5. Feature modules do not introduce an undocumented second locale set.
  6. Fallback resources exist in the default values directory.
  7. Locale identifiers use the same mapping at translation, build, and runtime boundaries.

Library resources deserve special attention. A dependency may contribute translations that make a locale appear supported even when the app's own critical strings are missing. Build your promised locale inventory from app policy, not from every qualifier found anywhere in the merged resource graph.

Make CI fail when the picker list, locale configuration, and release inventory disagree. A warning still allows the broken combination to ship.

Test the bundle that users receive

A debug APK cannot verify App Bundle delivery. Test a signed release bundle before promotion.

Android's current bundletool documentation supports device-specific APK sets, device-spec JSON files, configuration APK installation, and language as a size dimension. Use those capabilities to reproduce both the normal install and the missing-language edge case.

Build the release bundle first:

./gradlew :app:bundleRelease

Create a device specification that starts with only the default language:

{
  "supportedAbis": ["arm64-v8a"],
  "supportedLocales": ["en"],
  "screenDensity": 420,
  "sdkVersion": 35
}

Then create and install a targeted APK set:

bundletool build-apks \
  --bundle=app/build/outputs/bundle/release/app-release.aab \
  --output=/tmp/app-release.apks \
  --device-spec=/tmp/device-en.json

bundletool install-apks --apks=/tmp/app-release.apks

The values in the device specification must match a supported test target. The example is a test fixture, not a universal device profile.

After installation, exercise the language picker. The expected result depends on your chosen strategy:

  • With language splits disabled, the requested locale should render immediately because its resources are packaged.
  • With on-demand delivery, the app should request the missing language, show a bounded loading state, verify installation, and only then activate it.
  • Offline, the app should keep the last working locale and offer a clear retry path.

Run the same checks after force-stop, process recreation, device restart, and app upgrade. Release-only failures often survive recreation because the selection persisted while the required resource did not.

Inspect configuration APKs instead of guessing

Keep the generated .apks file as a CI artifact for the release candidate. It is a ZIP-compatible archive, so your build check can list its entries and verify whether language configuration APKs exist when splitting is enabled.

Filename matching is useful for diagnosis, but it is weak proof. Install the device-specific set and assert rendered values on a test-only screen. Tool versions may change the package layout without changing the behavior users need.

Put these fields on an internal test screen:

  • Requested locale
  • Effective application locale
  • Current system locale list
  • A translated sentinel string owned by the base module
  • A translated sentinel from each installed feature module
  • Whether the delivery layer considers the requested language installed

Keep this screen behind a debug or internal-test gate. It turns a vague report such as "French did not work" into evidence that distinguishes selection, resource ownership, and delivery.

Handle failures without corrupting locale state

Treat a language switch as a transaction. Track the previous working locale, the requested locale, resource acquisition, activation, and verification. The app must be able to recover if its process dies anywhere in that sequence.

Use these rules:

  • Never replace the last working locale until required resources pass a lookup check.
  • Treat a download callback as transport success, not rendering success.
  • Verify a known translated sentinel from every required module.
  • Keep default resources complete so an unexpected locale still has a safe fallback.
  • After an app upgrade, revalidate previously downloaded languages against the new version.
  • Log requested locale, effective locale, delivery state, app version, and failure category without recording private user data.

If activation fails, return to the prior locale and expose a retry. Do not silently pretend the switch succeeded. That creates an account preference that disagrees with the visible app and makes later support reports harder to diagnose.

Avoid these common release mistakes

Testing only Android Studio installs is the easiest way to miss this defect. Those installs often have a different resource shape from Play's optimized delivery.

LocaleConfig causes another common misunderstanding. It supports locale discovery and platform selection, but the bundle delivery policy controls which language APKs are present.

Changing enableSplit without measuring the artifact trades one unknown for another. Record the before and after compressed size for the same signed release candidate.

With on-demand delivery, applying the locale before the request finishes turns a recoverable network error into broken persisted state. Wait for a successful resource lookup.

The base module is only part of a modular app. If a translated screen belongs to a dynamic feature, test its language resources after feature installation and again after a locale change.

Add a release gate

Before shipping the next Android App Bundle, require one recorded answer to each question:

  • What is the canonical list of app-selectable locales?
  • Are language splits enabled or disabled?
  • If enabled, what downloads a language missing from the installed set?
  • Does the picker wait for verified resources before activation?
  • Does a signed device-specific APK set pass language switching?
  • Does the app preserve the last working locale when offline?
  • Do base and feature-module sentinel strings resolve correctly?
  • Does the same locale still work after process death and app upgrade?
  • Did the team compare compressed size across the chosen policy?
  • Is the test evidence tied to the exact release bundle?

Start by generating a device-specific APK set for your current signed bundle with only the default locale in supportedLocales. If your in-app picker cannot switch that install safely to another promised language, stop the release and choose an explicit delivery strategy before changing locale code again.

References