|
Knowledgebase
EnterprisePlatform
PlatformAPIReact (MCP)CLIIntegrationsReact (Lingo Compiler)
Alpha
Guides
Changelog

Localization

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

Workflows

  • Engine Setup with MCP
  • CI/CD

Android App Localization with strings.xml

Max PrilutskiyMax Prilutskiy·Updated 1 day ago·5 min read

The Lingo.dev CLI translates Android string resources (strings.xml) through a configured localization engine. The CLI's android bucket type 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 with GitHub Actions 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 a GitHub Actions workflow.

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 instructions apply. Create one in the Lingo.dev dashboard and generate an API key.

2

Verify Node.js

The CLI requires Node.js 18 or higher:

bash
node -v
3

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#

Create an i18n.json file in your project root. The android bucket tells the CLI to parse Android XML resources and create separate files per locale:

json
{
  "$schema": "https://lingo.dev/schema/i18n.json",
  "version": "1.15",
  "locale": {
    "source": "en",
    "targets": ["es", "fr", "de", "ja"]
  },
  "buckets": {
    "android": {
      "include": ["app/src/main/res/values-[locale]/strings.xml"]
    }
  }
}

The [locale] placeholder resolves to each configured locale code at runtime. The CLI substitutes your source locale into the pattern - so with source: "en", it looks for values-en/strings.xml. Target locales produce values-es/strings.xml, values-fr/strings.xml, and so on.

Bridge the Default Locale Directory#

Android stores default strings in values/ (no locale suffix), but the CLI resolves [locale] literally and looks for values-en/strings.xml. Create a symlink to bridge the two conventions:

bash
cd app/src/main/res
ln -s values values-en

This makes the source strings visible at both values/strings.xml (where Android expects them) and values-en/strings.xml (where the CLI looks). Commit the symlink to your repository - git tracks symlinks natively on macOS and Linux.

Windows

Git on Windows may check out symlinks as plain text files depending on your configuration. If you're on Windows, run git config core.symlinks true before cloning, or copy the values/ directory to values-en/ instead.

Multiple resource files

If your project splits strings across multiple files (for example, strings.xml and arrays.xml), list them all. The symlink covers the entire directory, so all files inside values/ are accessible through values-en/:

json
{
  "buckets": {
    "android": {
      "include": [
        "app/src/main/res/values-[locale]/strings.xml",
        "app/src/main/res/values-[locale]/arrays.xml"
      ]
    }
  }
}

Translate Locally#

Set your API key and run the CLI:

bash
export LINGO_API_KEY="your-api-key"
npx lingo.dev@latest run

The CLI reads your source strings.xml, identifies untranslated entries using the lockfile, 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.

To target a specific locale during development:

bash
npx lingo.dev@latest run --target-locale es

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
{
  "buckets": {
    "android": {
      "include": ["app/src/main/res/values-[locale]/strings.xml"],
      "lockedKeys": ["app_name", "api_base_url"]
    }
  }
}

Locked keys are copied from source to all target files without entering the translation pipeline.

Automate with GitHub Actions#

Add a workflow file at .github/workflows/translate.yml to translate on every push:

Translations commit directly to main - zero friction, ideal for small teams:

yaml
name: Translate
on:
  push:
    branches: [main]
permissions:
  contents: write
jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lingo.dev
        uses: lingodotdev/lingo.dev@main
        with:
          api-key: ${{ secrets.LINGODOTDEV_API_KEY }}

Store your API key as LINGODOTDEV_API_KEY in Settings > Secrets and variables > Actions in your GitHub repository.

Verify Before Deploy#

Use the --frozen flag as a deployment gate to ensure no untranslated strings ship to production. The CLI exits with a non-zero status if any entries need translation:

bash
npx lingo.dev@latest run --frozen

Add this as a separate CI step before your build:

yaml
- name: Verify translations
  run: npx lingo.dev@latest run --frozen

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?