I shipped an Android app localized into 15 languages. Fourteen of them worked.
Indonesian users got the app in French — the default locale — with no error,
no lint warning, and no crash anywhere. The Indonesian strings were in the
APK the whole time.
TL;DR — Android canonicalizes the Indonesian language code id to the
legacy Java code in at runtime, but AAPT2 does not canonicalize resource
folder names. A folder named res/values-id produces resources tagged id
that the resource matcher, looking for in, never selects. Rename the folder
to res/values-in. Same trap for Hebrew (values-iw, not values-he).
The setup
An offline Android game, minSdk 30, localized the standard way: one
res/values-<code>/strings.xml per language, a locales_config.xml
declaring the supported locales with modern BCP-47 tags (<locale for Indonesian), and language switching through
android:name="id"/>
AppCompatDelegate.setApplicationLocales(). Indonesian is one of the
languages whose ISO 639-1 code was renamed decades ago: the modern code is
id, but Java — and therefore Android — still uses the pre-1989 code in
internally.
What I expected
That res/values-id is the correct folder name. Everything points that way:
the modern code is id, locales_config.xml requires the modern tag (lint
flags mismatches between the locale config and your resource folders), and
the build accepts values-id without a murmur. The APK genuinely contains
the translations:
aapt2 dump resources app-debug.apk | grep -A3 home_quick_play
# () "Jeu immédiat" <- default (French)
# (id) "Main langsung" <- Indonesian, present in the APK
# (de) "Sofort spielen"
What actually happens
At runtime the Indonesian config is never matched. I tried every way of
asking for Indonesian on an API 34 emulator and an API 37 emulator:
adb shell cmd locale set-app-locales com.example.app --user 0 --locales id
adb shell cmd locale set-app-locales com.example.app --user 0 --locales id-ID
adb shell cmd locale set-app-locales com.example.app --user 0 --locales in
All three: the app renders in the default language. The in-app selector
calling AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags("id"))
fails the same way — the preference is stored, the picker shows « Bahasa
Indonesia » as selected, and every string on screen is French. Nothing logs.
Note the scope: only Android resources fail. Text my code loads from asset
files, keyed off Locale.getLanguage() with a manual in → id mapping,
displayed in Indonesian the whole time — which made the half-translated
screen genuinely confusing.
The fix
Rename every values-id folder to values-in and rebuild. Nothing else
changes; locales_config.xml keeps the modern id tag.
find . -type d -name "values-id" -not -path "*/build/*" \
| while read d; do git mv "$d" "${d%values-id}values-in"; done
./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell cmd locale set-app-locales com.example.app --user 0 --locales id
After that, the same set-app-locales id command produced an Indonesian UI
on the first launch — I verified the switch on the age-gate screen of a
fresh install, which went from French to « Berapa usia Anda? ».

Why it works
Java froze its language codes before ISO renamed three of them, and
java.util.Locale still canonicalizes to the old codes for backward
compatibility: new Locale("id").getLanguage() returns "in" (likewise
he → iw and yi → ji). Android’s resource matcher compares that
canonicalized code against the config tags baked into the APK.
AAPT2 sits on the other side of the gap: it takes the folder suffix as the
config tag more or less literally, so values-id becomes config id — a
tag the matcher will never be asked for, since every lookup arrives
pre-canonicalized as in. values-in becomes config in and matches. The
two halves of the toolchain disagree about which spelling is canonical, and
neither one warns about the orphaned spelling.
The asymmetry is what makes the bug silent: an invalid folder name fails
the build, but id is a perfectly valid BCP-47 tag, so it builds, installs,
and simply never wins resolution.
What I did not test
Physical devices — both checks ran on emulators (API 34 stable and an API 37
preview image), though the mechanism is in Locale canonicalization and
should not vary by device. App bundles with Play’s per-language splits, where
the split naming might interact differently. Hebrew and Yiddish, which I
infer from the same Locale canonicalization list but did not run. And I
did not check whether any AGP version newer than mine warns about this.
Facts
context: Android app localized via res/values-<code> folders, Indonesian among the languages
problem: values-id builds and ships but is never matched at runtime; users get the default language
solution: rename the folder to values-in (legacy Java code); keep "id" in locales_config.xml
verified_on: 2026-08-18
applies_to: [Android API 34, Android API 37 preview, AGP with Kotlin 2.2, framework and AppCompat per-app locales]
does_not_apply_to: [strings loaded from assets by app code, languages whose ISO code never changed]
Ping : Compose 1.12 forces AGP 9, and AGP 9 breaks kotlin-android — remove the plugin, don't upgrade it