File formats & standardsこの記事を日本語で読む

Android string resources: strings.xml, plurals, and the qualifier system

Android text lives in resource files, not in code. Every string a user can see is declared once in an XML file and referenced from layouts and Kotlin or Java by a stable name. This separation is what makes translation possible without touching a single line of application logic: a translator opens an XML file, changes the text inside the tags, and the app picks the right file at runtime based on the device's language.

The mechanism behind that runtime choice is the resource-qualifier directory scheme, and it is worth understanding precisely, because most localization bugs on Android come from a qualifier being slightly wrong rather than from bad translation.

The basic key-value format

A string resource file is XML with one root element and a flat list of entries. Each entry has a name attribute — the key referenced elsewhere in the app — and the translatable text as its element content.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="welcome_title">Welcome back</string>
    <string name="logout_confirm">Are you sure you want to log out?</string>
</resources>

The qualifier directory scheme

Translations do not live in one file with a language column. They live in parallel files at the same relative path, each inside a res/values directory whose name carries a qualifier suffix — a two-letter language code, optionally followed by a region.

res/values/strings.xml          (default — used when nothing else matches)
res/values-de/strings.xml        (German)
res/values-fr/strings.xml        (French)
res/values-pt-rBR/strings.xml    (Portuguese, Brazil region)
res/values-zh-rTW/strings.xml    (Chinese, Traditional / Taiwan)

At runtime the system resolves the device's locale against these directories and falls back to values/strings.xml — the default resource, with no qualifier — for any key missing from the more specific one. This fallback is quiet: a missing translation does not error, it just silently shows the default-language text, which is why checking for missing keys across all values-* directories has to be a deliberate step rather than something you notice from a crash.

Every key that exists in the default file should exist in every language file with the same name. A key present in French but not in German is not a German bug you will see locally — it only surfaces on a device set to German.

Plurals and quantity keywords

English plural handling looks trivial — one form for singular, one for everything else — but that rule does not hold across languages. Some languages have three, four, or six plural categories with different rules for which numbers fall into which category. Android's plurals resource lets you supply one string per grammatical category instead of hardcoding an if/else around a count.

<plurals name="items_selected">
    <item quantity="one">%d item selected</item>
    <item quantity="other">%d items selected</item>
</plurals>

The quantity attribute is not a free label — it must be one of a fixed set of keywords (zero, one, two, few, many, other) defined by the Unicode CLDR plural rules for each language. Which of those categories actually apply, and which numbers map to which category, differs by language: a language with no special dual form simply never uses two, and other is the only category every language requires, since it is the catch-all. A translator working on a Slavic language with several plural categories needs every applicable item present, not just one=/other= copied from the English file.

Escaping and format arguments

Because the file is XML, characters with special meaning to the XML parser — the ampersand and the angle brackets — must be written as entities inside the text, not as literal characters. Two more characters need attention for a different reason: Android's own string-resource parser treats an apostrophe or a straight double quote as meaningful unless it is escaped, because those characters can otherwise be confused with quoting used elsewhere in the resource system.

  • Apostrophes inside text need a backslash before them, or the whole string wrapped in double quotes
  • Straight double quotes inside text need a backslash before them
  • XML-reserved characters (&, <, >) need their XML entities regardless of the apostrophe/quote rule

Positional format arguments follow the standard printf-style syntax — %s for a string, %d for an integer — and Android additionally supports a positional index so a translator can reorder arguments to match the target language's grammar without the app code changing.

<string name="greeting">Hello, %1$s! You have %2$d new messages.</string>

%1$s and %2$d can appear in either order in the translated string; the number picks which argument fills the slot, independent of position in the sentence. This matters constantly in translation: many languages put the subject or the count in a different place than English does, and without positional indices a translator would have no way to reorder the sentence at all.

Marking strings as non-translatable

Not every entry in a strings.xml file is user-facing prose. Analytics event names, internal log tags, and format strings consumed only by other code often live in the same file for convenience, and translating them would break the app rather than help a user. Android's translatable attribute tells the tooling — and any human reviewing the file — to leave that entry alone.

<string name="analytics_event_login" translatable="false">login_event</string>

Marking these correctly matters for more than tidiness: translation tools that scan the file for work to do will otherwise hand a translator a string that was never meant to be read by a user, wasting their time and risking a translated value being fed into code that expects an exact literal.

Related articles