Android App Localization: Folders, Defaults, Play Listing
Android App Localization: Folders, Defaults, Play Listing
Android app localization comes down to four jobs. Put every user-facing string in res/values/strings.xml, add a translated copy in a values-<lang> folder for each language, test with a pseudolocale before you pay for translations, and paste the Play listing text into Play Console separately, because your resource files don't feed the store page.
Most tutorials skip the details that bite. Your default values/strings.xml has to contain every string, or the app fails in any language you didn't plan for. Put each language in a parent folder like values-es, not only in a region folder like values-es-rMX, because older Android versions won't fall back between regions.
On Android 13 and later, users can pick your app's language in system settings, and you don't need the old ContextWrapper trick to support that. Play listing text has its own limits: 30 characters for the title, 80 for the short description, 4,000 for the full description.
The steps below follow the order you'd do them on a real project. Escaping, plurals and RTL layout attributes are in my walkthrough on translating strings.xml, so I don't repeat them here.
1. Move every string into res/values/strings.xml
Open each layout and each Kotlin or Java file and look for text in quotes that a user will see. Button labels, toasts, error messages, notification titles, empty-state copy. Each one gets a name in res/values/strings.xml and a reference (@string/save in XML, getString(R.string.save) in code).
This file is your default. Android reads it whenever the device language has no matching folder. Keep it complete and in your source language, usually English.
2. Keep the default file complete, or the app crashes
The Android docs give a precise example of the crash people hit after "adding a translation." An app has text_a and text_b in res/values-en/strings.xml, but the default res/values/strings.xml only defines text_a. The app runs fine in English. In every other language it force-closes, because Android falls back to the default file, can't find text_b, and has nothing to show.
You get the same failure if the default file is missing entirely. A complete values-en folder doesn't save you, since a French or Japanese device never looks there.
The rule I follow: new strings go into values/strings.xml first, every time. Translated folders can lag behind by a release. The default can't.
3. Create parent language folders, not only region folders
Folder names follow a fixed pattern from the resource qualifier rules:
| What you want | Folder |
|---|---|
| Spanish (any region) | values-es |
| Spanish for Spain | values-es-rES |
| Latin American Spanish (Android 7.0+) | values-b+es+419 |
| Traditional Chinese (Android 7.0+) | values-b+zh+Hant |
The language part is an ISO 639-1 code. Region adds a lowercase r plus an ISO 3166-1 code. The b+ form arrived in Android 7.0 (API 24) and uses plus signs where the older form uses hyphens.
The trap is shipping only a region folder. Before Android 7.0, a phone set to es_MX with only values-es-rES in your app strips the country, looks for es, finds nothing, and shows your English defaults. It doesn't borrow Spain's Spanish. From Android 7.0 on, Android does try sibling regions when the exact locale and the parent both miss, so a fr_CH phone with only fr_FR gets fr_FR. That's friendlier, but your app now behaves differently on old and new phones.
Google's own advice is to store strings in the most common parent: values-es instead of only a region folder, and values-b+es+419 for Latin American Spanish instead of only values-es-rUS. Add region folders later for the few strings that differ.
This is the most likely answer if your app is still in English after you added Spanish. Check the folder name against the device's language and region.
4. Lock placeholders before anyone translates
A string like Hi %1$s, you have %2$d new messages breaks if a translator touches %1$s. Wrap tokens that must survive in xliff:g, with an id that names the hole and an example that shows a sample value. That's a different job from translatable="false", which removes the whole string from translation (an API key name, a brand name that never changes).
Plurals, apostrophe escaping and positional arguments have their own rules. The strings.xml translation guide has them with examples.
5. Test with a pseudolocale before paying for translations
This step catches most problems for free. Enable pseudolocales in your debug build:
android {
buildTypes {
debug {
pseudoLocalesEnabled true
}
}
}
In Kotlin DSL the flag is isPseudoLocalesEnabled = true. You need Android 4.3 (API 18) or higher and developer options turned on. Then pick the pseudolocale in Settings like any other language.
English (XA) adds accents to your English text, pads it with extra words and wraps each message in brackets. Click through every screen. Plain, unaccented text is a hardcoded string you missed in step 1. Text that gets cut off or pushes a button off screen shows you where a longer language will break the layout.
AR (XB) flips left-to-right messages to right-to-left, so you can see which screens fall apart in Arabic or Hebrew before you ship either.
I'd do this before sending the file anywhere. If you find hardcoded strings after the translations come back, you pay again to translate the strings you add.
6. Add per-app language without a ContextWrapper
The top tutorial for this query still teaches a custom ContextWrapper with updateConfiguration and a language dialog. That was the workaround before Android had a real API. Don't build it now.
Android 13 (API 33) added per-app language preferences: a system setting for each app, plus LocaleManager.setApplicationLocales and getApplicationLocales if you want an in-app picker. Those calls stay in sync with system settings. For Android 12 (API 32) and earlier, AndroidX has backward-compatible equivalents. Google says to use them through AppCompat 1.6.0 or higher, and they work with the AppCompatActivity context, not the application context.
Your languages only appear in that system setting if the app declares them. Adding a values-* folder isn't enough on its own. You have two options:
- Let the build generate it. Android Gradle Plugin 8.1.0 builds the locale config from your
resfolders whenandroidResources.generateLocaleConfigis true. Google recommends this path. - Write
res/xml/locale_config.xmlyourself and reference it withandroid:localeConfigin the manifest.
7. Paste the Play listing separately
Translating strings.xml doesn't touch your Play Store page. Listing text lives in Play Console under Store listings, then Manage translations, and Google doesn't read it from your resource files.
| Field | Limit |
|---|---|
| App title | 30 characters |
| Short description | 80 characters |
| Full description | 4,000 characters |
The limits apply to full-width and half-width characters alike, and Google also lists them on the app setup page. A title that fits in English can run past 30 characters once translated, so check each one. If you skip a language, Google may show an automated translation of your listing, which you don't get to review.
Common mistakes
- Adding a string to
values-esbut notvalues. Works on your Spanish test phone, crashes for everyone else. - Shipping only
values-es-rMX. Phones running a version before Android 7.0 in any other Spanish region get English. - Copying the 2021
updateConfigurationcode. You end up maintaining your own locale plumbing when Android 13 and AppCompat 1.6.0 already handle it. - Expecting the system language picker to find your folders on its own. Turn on
generateLocaleConfigor addlocale_config.xml. - Translating with placeholders unprotected. One mangled
%1$scan crash a screen at runtime. - Treating the Play listing as done because the app is translated. It's a separate paste with a 30-character title.
If you also ship on iOS, the same ideas apply with different files. My iOS localization page covers that side, the localization file formats page explains what each platform's files look like, and the broader app localization guide compares the platforms.
Filling the values folders without a subscription
After steps 1 to 5, the remaining chore is producing values-de/strings.xml, values-ja/strings.xml and the rest without breaking anything. I built Localize Your App for that part, because it was the release chore I kept postponing.
You upload your strings.xml, and it translates into all 50 languages it supports or a subset you choose. Placeholders are locked. A glossary keeps product terms consistent, and translation memory reuses approved strings, so repeats are free. You review source and target side by side, edit any line, and download the same format. Dropping each file into the matching values-* folder is still your job.
It doesn't submit your app to Play, and it doesn't write your listing into Play Console. It can export store-listing copy you paste yourself. There's no subscription, no human translators and no CLI. Starter is $9 once for 8,000 credits, which the site says covers about a 200-string app in all 50 languages.
FAQ
Why is my Android app still in English after I added a Spanish strings.xml?
The folder name probably doesn't match the device. If you only have values-es-rES and the phone is set to Mexican Spanish on a version before Android 7.0, Android skips your file and uses the defaults. Add a values-es parent folder.
Does translating strings.xml also translate the Google Play listing?
No. The Play listing is a separate set of fields in Play Console under Store listings, with limits of 30, 80 and 4,000 characters. Google may show an automated translation if you leave a language empty.
How do I let users pick a language inside the app without changing the phone language?
On Android 13 and later, use LocaleManager.setApplicationLocales. For older versions, use the AndroidX equivalents through AppCompat 1.6.0 or higher with an AppCompatActivity context. Declare your languages with a generated or manual locale config so they show up in system settings too.
Why did the app crash after I added a translation?
A string exists in a translated folder but not in the default res/values/strings.xml. Any device whose language falls back to the default can't find it, and the app force-closes. Add the string to the default file.
How do I find hardcoded strings before sending the file for translation?
Turn on pseudolocales in the debug build and switch the device to English (XA). Any text that shows up without accents and brackets is still hardcoded in a layout or in code.
Once your default file is complete and the pseudolocale run looks clean, you're ready for real translations. Upload your strings.xml and translate your first file.
Ship your app in 50 languages by tonight
Upload your localization file, review side by side, download ready-to-import files for every language. One-time credits from $9 — no subscription.
No subscription. Credits never expire.