|
Documentation
Book a DemoPlatform
PlatformMCPCLIAPIWorkflows
Guides
Changelog

Localization

  • Overview
  • Translation API
  • Web App Localization
  • Mobile App Localization
  • iOS with String Catalogs
  • Android with strings.xml
  • Emails Localization
  • Static Content (e.g. .md, .json)
  • Next.js with Markdoc
  • Rails with i18n

Workflows

  • Engine Setup with MCP
  • Jira Triage
  • CI/CD

Android App Localization with strings.xml

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.

text
app/src/main/res/
  values/              # Default (source) strings
    strings.xml
  values-es/           # Spanish
    strings.xml
  values-fr/           # French
    strings.xml
  values-ja/           # Japanese
    strings.xml

A typical strings.xml contains three element types:

xml
<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#

1

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.

2

Verify Node.js

The CLI requires Node.js 22 or higher:

bash
node -v
3

Install the CLI

Install the CLI globally, which exposes the lingo command:

bash
npm install -g @lingo.dev/cli
4

Sign in

Authenticate with a one-time password:

bash
lingo login

For CI, use an API key instead - pass --api-key or set LINGO_API_KEY.

5

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:

json
{
  "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:

json
{
  "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.

LocaleResource directory
en (source)values/
esvalues-es/
pt-BRvalues-pt-rBR/
zh-Hansvalues-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:

bash
lingo push --backfill-missing

The 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:

bash
lingo push

To 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:

bash
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:

xml
<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:

json
{
  "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:

yaml
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:

bash
lingo check

Add this as a separate CI step before your build:

yaml
- name: Verify translations
  run: lingo check
  env:
    LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}

Next Steps#

Mobile App Localization
Overview of all mobile platforms - iOS, Android, Flutter, React Native
CI/CD Workflows
GitHub Actions, GitLab CI, Bitbucket Pipelines patterns
Glossaries
Lock brand names and technical terms from translation
Key Locking
Copy specific values without translating them

Was this page helpful?

Max PrilutskiyMax Prilutskiy·Updated 12 days ago·6 min read