Android's localization model is built on resource files, not code. Every user-facing string lives in res/values/strings.xml, referenced by a stable name. Localizing an Android app means producing a parallel strings.xml for each target locale — same keys, translated values, same format in and out. strings.dev works on that native file directly: you upload strings.xml, pick target locales, and get native strings.xml back, with no XLIFF conversion step in the middle to corrupt escaping or placeholders.
What "Android app localization" actually covers
The phrase gets stretched to mean everything from Play Store listings to server copy. Be precise: this page — and strings.dev — is about in-app strings, the values in your strings.xml resource files that the app renders at runtime.
Two things it does not cover, and you shouldn't conflate them:
- Play Console listing metadata (title, short/full description, screenshots, keywords) is translated in the Google Play Console, not in your resource files. That's a separate task.
- Server-driven or web content your app happens to display isn't in
strings.xml, so it's outside this loop.
Everything below is about the resource strings you ship inside the APK/AAB.
Anatomy of strings.xml
A strings.xml file is a flat <resources> document. The three element types you localize are <string>, <plurals>, and <string-array>:
<!-- res/values/strings.xml (base, English) -->
<resources>
<string name="welcome_title">Welcome back</string>
<string name="save_button">Save</string>
<!-- Marked untranslatable: stays in every locale -->
<string name="brand_name" translatable="false">Acme</string>
</resources>
Two attributes carry localization signal. name is the key your code and layouts reference (R.string.save_button) — it must stay identical across every locale file. translatable="false" tells tools (and strings.dev) to leave a value alone — use it for internal keys, format-only strings, and fixed brand tokens. strings.dev honors translatable="false" and additionally lets you mark brand phrases as protected so a product mode named "Send" never becomes a French verb.
Plurals and string-arrays
English collapses quantity to "1 item / 2 items," but Android follows the Unicode CLDR plural categories (zero, one, two, few, many, other). You express that with <plurals>, and read it with getQuantityString():
<plurals name="cart_item_count">
<item quantity="one">%d item in cart</item>
<item quantity="other">%d items in cart</item>
</plurals>
val text = resources.getQuantityString(R.plurals.cart_item_count, count, count)
The subtlety that breaks apps: a target language may require categories your English source never defined. Arabic uses all six; Russian and Polish need distinct few and many forms. A correct localization step generates the categories each locale actually requires rather than mirroring English's one/other. strings.dev expands <plurals> per locale into the forms that language's CLDR rules demand.
<string-array> holds ordered lists — spinner options, onboarding steps — where item order is load-bearing:
<string-array name="difficulty_levels">
<item>Easy</item>
<item>Medium</item>
<item>Hard</item>
</string-array>
Both <plurals> and <string-array> round-trip in the native file, so item order and quantity keys are preserved exactly.
Placeholders and escaping preserved
Interpolated values are where naive translation corrupts a build. Android uses printf-style tokens: %s (string), %d (integer), and positional forms %1$s, %2$d when a string has more than one argument. Positional forms matter because German and Japanese often reorder the arguments — the translation must be free to move %2$d ahead of %1$s without breaking:
<string name="msg_summary">%1$s sent you %2$d files</string>
Whenever a string contains any format argument, a literal percent must be written %%, or the string marked formatted="false". strings.dev preserves placeholder tokens, positional indices, URLs, emails, and @handles untouched, and lets the translation reorder positional arguments where the target grammar needs it.
Android's XML escaping rules are their own class of bug, because several characters are interpreted by the resource compiler rather than displayed:
| Character |
Why it's special |
Escaped form |
' apostrophe |
Bare apostrophe throws a compile error |
\' or wrap the whole value in "…" |
" quote |
Delimits literal whitespace |
\" |
@ at value start |
Reads as a resource reference |
\@ |
? at value start |
Reads as a theme-attribute reference |
\? |
< & |
XML markup |
< & (or CDATA) |
Leading and trailing whitespace is also collapsed unless the value is wrapped in double quotes. This is exactly where an XLIFF round-trip or a copy-paste-into-a-spreadsheet workflow silently mangles strings — an unescaped apostrophe in a French or Italian translation ("l'application," "un'app") is the classic broken build. Because strings.dev reads and writes the native strings.xml, it applies Android's escaping rules to translated values so the file compiles as-is.
Resource folders: values-xx and values-b+
Android selects the right file at runtime by directory qualifier, not by anything inside the file. Your base lives in res/values/; each locale gets a parallel folder:
res/
├── values/ strings.xml (base / default)
├── values-de/ strings.xml (German)
├── values-es/ strings.xml (Spanish)
├── values-es-rMX/ strings.xml (Spanish, Mexico)
├── values-pt-rBR/ strings.xml (Portuguese, Brazil)
└── values-b+zh+Hans/ strings.xml (Chinese, Simplified script)
There are two qualifier syntaxes. The older values-es-rMX form uses a lowercase language and an r-prefixed region. The newer BCP 47 form, values-b+, handles scripts and complex tags — values-b+zh+Hans for Simplified vs values-b+zh+Hant for Traditional Chinese. Getting these qualifiers right is what lets Android resolve es-MX distinctly from es-ES. strings.dev's 52 locales include the regional and script variants — es-ES/es-MX, pt-BR/pt-PT, zh-Hans/zh-Hant, fr-FR/fr-CA — that map onto these folders; the full list and qualifier reference is in the locale-codes reference.
One format across Android, Android TV, Wear OS & Android Auto
The payoff of a resource-based model: the same strings.xml mechanism covers every Android form factor. Android TV, Wear OS, and Android Auto apps all draw their user-facing text from res/values*/strings.xml using the same <string>, <plurals>, and folder-qualifier rules — a TV leanback launcher label, a watch complication string, and a phone button label are the same kind of resource. There's no separate localization format to learn per surface. Localize the strings.xml once and every target — phone, tablet, TV, Wear, Auto — gets its translated resource folder from the same pipeline.
Send strings.xml, get strings.xml back
Every project generates a per-project CLI script (plus a REST API key and the strings-mcp-server MCP package) so the loop fits your existing tooling. A minimal push-then-pull, wired into a git hook or CI stage, keeps translations in sync with the code:
# Generated per-project script — push changed base strings, pull translations
strings push res/values/strings.xml
strings pull --into res/
Every translation is fed your app-description context and any per-string translation notes, so output reflects what the app does rather than dictionary-literal guesses. Full setup lives in the CLI and API docs.
The free Indie tier covers 1 project and 1 language with unlimited word translations — enough to localize an Android app into a first market end to end. Going wider (unlimited languages, 2 projects) plus brand context and the QA & analytics dashboard that flags failed translations for retry is Indie Plus, $20/mo — or $10/mo billed annually. For the iOS side of the same loop, see the platform hub and supported languages.