The Lingo.dev CLI translates Android string resources (strings.xml) through a configured localization engine. With the android format, the CLI understands <resources>, <string>, <string-array>, and <plurals> elements natively, preserving XML structure and generating correct plural categories for each target locale.
This guide walks through localizing an Android app end-to-end: configuring the CLI, translating locally, and automating in CI so translations ship on every push.
Demo repository
Clone or fork lingodotdev/android-app-localization-example to follow along. The repository contains a working Android project with string resources, a Lingo.dev CLI configuration, and translations committed for every target locale.
How Android Localization Works#
Android uses a resource directory convention where each locale gets its own values-[locale]/ directory. The system loads the correct strings.xml at runtime based on the device's language setting.
app/src/main/res/
values/ # Default (source) strings
strings.xml
values-es/ # Spanish
strings.xml
values-fr/ # French
strings.xml
values-ja/ # Japanese
strings.xmlA typical strings.xml contains three element types:
<resources>
<!-- Simple strings -->
<string name="app_name">My App</string>
<string name="welcome_message">Welcome back!</string>
<!-- String arrays -->
<string-array name="planets">
<item>Mercury</item>
<item>Venus</item>
<item>Earth</item>
</string-array>
<!-- Plurals -->
<plurals name="items_count">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
</resources>The CLI parses all three element types, translates their content through the localization engine, and writes per-locale files into the correct values-[locale]/ directories.
Prerequisites#
Create a localization engine
Every CLI run sends content through a localization engine - the configuration that determines which LLM model, glossary, brand voice, and rules apply. Create one in the Lingo.dev dashboard.
Verify Node.js
The CLI requires Node.js 22 or higher:
node -vInstall the CLI
Install the CLI globally, which exposes the lingo command:
npm install -g @lingo.dev/cliSign in
Authenticate with a one-time password:
lingo loginFor CI, use an API key instead - pass --api-key or set LINGO_API_KEY.
Set up your Android project
Your project needs a default strings.xml in app/src/main/res/values/. Android Studio creates this file when you start a new project. See Android's localization guide for setting up resource directories.
Configure the CLI#
Run lingo init in your project root to create .lingo/config.json with your source and target locales and file patterns, then lingo link to attach your organization and engine. The result looks like this:
{
"orgId": "org_...",
"engineId": "eng_...",
"sourceLocale": "en",
"targetLocales": ["es", "fr", "de", "ja"],
"files": [
{
"pattern": "app/src/main/res/values/strings.xml",
"format": "android"
}
]
}The pattern points at your default resource directory - the unqualified values/, exactly where Android expects the source strings. No locale code appears in it, and none is needed.
Why `format` is set explicitly
The CLI auto-detects most formats from the file extension, but .xml is ambiguous, so Android resource files need an explicit "format": "android" on the files entry.
Multiple resource files
If your project splits strings across multiple files (for example, strings.xml and arrays.xml), add a files entry for each:
{
"files": [
{
"pattern": "app/src/main/res/values/strings.xml",
"format": "android"
},
{
"pattern": "app/src/main/res/values/arrays.xml",
"format": "android"
}
]
}Commit .lingo/config.json to your repository.
Locale Directories and Qualifiers#
Android keeps the default language in an unqualified values/ directory, so the source path carries no locale code. The CLI recognises this: it treats bare values/ as the source locale and appends the target qualifier for every other locale.
| Locale | Resource directory |
|---|---|
en (source) | values/ |
es | values-es/ |
pt-BR | values-pt-rBR/ |
zh-Hans | values-b+zh+Hans/ |
Regional and scripted locales are worth understanding here, because a resource qualifier is not a raw BCP 47 tag. Android accepts two spellings: the legacy language-region form (values-pt-rBR/), and a BCP 47 form prefixed with b+ (values-b+pt+BR/, API 24 and above). A directory named values-pt-BR/ is ignored outright - the strings would exist and never load.
Setting "format": "android" makes the CLI emit the right spelling for you: the legacy form wherever it can express the locale, and b+ for scripts, three-letter languages, and numeric regions.
Upgrading from an older setup
Earlier CLI versions required the locale to appear in the source path, and this guide used to recommend a values-en -> values symlink to bridge the two conventions. From @lingo.dev/cli 1.12.0 that is no longer necessary - point the pattern at values/strings.xml and delete the symlink.
Translate Locally#
Run the CLI. On the first run - or whenever you add a new target locale - use --backfill-missing so every existing string is translated:
lingo push --backfill-missingThe CLI reads your source strings.xml, identifies untranslated entries using the run state, translates the delta through your localization engine, and writes results into the target values-[locale]/ directories. Open any target file to see the translated strings.
On subsequent runs, lingo push translates only what changed:
lingo pushTo scope a run to specific files, pass a glob. Patterns are matched against source paths, so scope by the source file rather than a target:
lingo push "app/src/main/res/values/strings.xml"To fetch translations produced elsewhere (for example, by CI) into your working tree, run lingo pull.
Plurals#
Android uses <plurals> elements with CLDR quantity strings (zero, one, two, few, many, other) to handle plural forms. Different languages require different plural categories - English needs two (one and other), Russian needs four, and Arabic needs six.
The CLI preserves the <plurals> structure during translation and generates the correct quantity entries for each target locale. A source entry with two categories:
<plurals name="messages_count">
<item quantity="one">%d new message</item>
<item quantity="other">%d new messages</item>
</plurals>Produces the correct categories for each target language. The localization engine knows which CLDR plural rules apply to each locale and generates only the categories that language requires.
Key Locking#
Some string values should stay identical across all languages - brand names, API endpoints, or format patterns. Use key locking to copy these values without translation:
{
"files": [
{
"pattern": "app/src/main/res/values/strings.xml",
"format": "android",
"lockedKeys": ["app_name", "api_base_url"]
}
]
}Locked keys are copied from source to all target files without entering the translation pipeline.
Automate in CI#
The recommended way to keep translations up to date is the Lingo.dev GitHub App. It runs server-side, reads your committed .lingo/config.json and engineId, and opens translation updates automatically - no runner, no stored secret, and no lockfile management on your side. Install it and point it at your repository to translate on every push.
If you prefer to run the CLI inside your own pipeline, add a workflow that installs the CLI and runs lingo push:
name: Translate
on:
push:
branches: [main]
permissions:
contents: write
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm install -g @lingo.dev/cli
- run: lingo push --backfill-missing
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}Store your API key as LINGO_API_KEY in Settings > Secrets and variables > Actions in your GitHub repository, then commit the updated target files (or open a pull request) as a follow-up step.
Verify Before Deploy#
Use lingo check as a deployment gate to ensure no untranslated strings ship to production. The command exits with a non-zero status if any entries need translation:
lingo checkAdd this as a separate CI step before your build:
- name: Verify translations
run: lingo check
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}