Two file types carry most mobile translation work: the Android strings.xml resource file and the XLIFF interchange file that translation tools exchange. A translated copy of either one can be perfectly readable to a person and still be wrong for the machine that reads it next. In strings.xml the usual culprits are an unescaped apostrophe, a placeholder that was reworded, an @ or ? that Android treats as special, a plural list that follows the source language instead of the target one, and markup that lost its protection. In XLIFF the usual culprits are a missing or doubled target, a changed inline code, a state value that tells the tool the wrong thing, and a language attribute that was never filled in. This guide lists those breaks for both formats and what a translation has to leave alone.

Escaping in strings.xml: the file is read twice

An Android string is processed in two passes. First the file is parsed as XML, then Android applies its own rules for whitespace and escapes. The documentation says that characters with a special meaning in XML must be escaped by the standard XML or HTML rules, and that a character with a special meaning in Android takes a preceding backslash (Android string resources). It also warns, in the description of each text-bearing element, that apostrophes and quotation marks must be escaped.

The same page gives the working forms: \@ and \? for those two characters, \n for a new line, \t for a tab, \uXXXX for a Unicode character, \' for a single quote and \" for a double quote. As an alternative to \' the whole string may be wrapped in double quotes. Wrapping in single quotes does not work for a double quote (Android string resources).

What this means for a translator:

  • Apostrophes appear in translations the source never had. English "Sign in" has none, but French "l'application" or Italian "un'altra" do. Every one of them needs the backslash or the quoting, and a model that writes natural French without thinking about the resource file produces exactly that.
  • Raw XML characters. An ampersand or a less-than sign in running text has to be written as an entity, because the XML parser comes first.
  • Starting characters. The documentation lists @ and ? as characters needing a backslash. We did not run a build to see where Android objects, so treat the rule as written and escape them wherever they could begin a value.
  • Whitespace. By default Android collapses runs of whitespace into one space, and double quotes preserve the spacing inside them. Because collapsing happens after XML parsing, entity-written spaces collapse too (Android string resources). A translator who aligns text with extra spaces loses them silently.

We have not run aapt2 or a Gradle build for this guide. The documentation says these characters must be escaped, and a build is the place to confirm what a given toolchain version does with each.

Placeholders: spelled the same, reordered by position

The documentation formats strings with arguments written as %1$s and %2$d in the resource (Android string resources). The number and the type letter are part of the program's contract: the code passes arguments in a fixed order. A translation may move a placeholder to a different place in the sentence, which is the point of the numbered form, but it may not rename it, change its type letter, drop it or invent a new one. When a translator turns %1$s ... %2$d into a sentence where the number comes first, writing %2$d first and %1$s second is correct and plain %s %d in the new order is not, since the plain form takes arguments in sequence and would swap them.

To keep a value out of the translator's hands, the localization guide recommends an xliff:g tag around it, with an id that says what the value is and an example that shows a typical value. The same page shows the tag around a Unicode escape, a web address, a product name and a quoted code, and the file has to declare the XLIFF namespace on its root element for the tag to be valid (Android localization guide). The content of such a tag is copied, the text beside it gets translated, and the tag may move to wherever the word order of the target language puts it.

Markup inside strings

The string-resource page lists the supported HTML-style tags, such as bold, italic, underline, line break and list items (Android string resources). Two rules for translators follow. The tags stay, in matching pairs, and only the words between them change. And when a styled string is also a format string, the documentation describes a workaround whose tags have escaped angle brackets and are recovered after formatting. In that form the opening bracket is the entity for less-than, so a translator who "tidies" it into a literal tag turns styling into a build or runtime problem. Keep the escaped form exactly.

Plurals: the categories belong to the target language

The documentation says Android supports six quantity categories, running from zero through one, two, few and many up to other, that the choice is made from grammar rather than from the number alone, and that a language such as Korean only ever uses other (Android string resources). It also notes that in English a zero item is ignored even when the count is zero, because zero behaves like any number except one.

So a translation cannot copy the English list of items. The CLDR chart defines the real rules per language: Polish has one, few, many and other, with few covering endings such as 2 to 4 except the teens, while Arabic uses all six categories and Japanese only other (Unicode CLDR plural rules). Translating English one and other into Polish with just two items leaves the few and many cases without a string of their own. The inverse mistake is as bad: items that the language never selects are dead weight, and an item whose text lacks the number loses its meaning in languages where one and a hundred and one share a form, as the Android page shows with a Lithuanian example. Each plural item also follows every rule above: escapes, placeholders and markup. Models make the same slips in other resource files too; a write-up of what broke when an LLM translated locale files shows how often placeholders and plural slots are the casualties.

Our own pages did not list how Android treats a resource that is marked as not translatable, or what a build does with a missing category, so we leave those cases out rather than guess.

XLIFF 1.2: what the importing tool compares

XLIFF is a different kind of file. A tool exports source texts, someone adds translations, and the tool imports them by matching units. The OASIS specification fixes the structure that has to survive (XLIFF 1.2 specification).

Source and target. Inside a translation unit, the text to translate is in source and the translation in a target placed with it. The specification recommends that content in a file be uniformly bilingual: the source in the language named by the file's source-language attribute and the target in the language named by its target-language attribute, with no xml:lang on those elements. If the target-language attribute is absent from the file element, the tool has nothing to compare, so a translation that fills in targets also fills in that attribute when it is missing.

State. A target may carry a state attribute. The pre-defined values include new, needs-translation, needs-review-translation, translated, signed-off and final, and user-defined values must start with x- (XLIFF 1.2 specification). Reading these values correctly matters: a target marked final or signed-off has passed a reviewer and is not a draft to overwrite, while new or needs-translation marks a unit that still needs work. A draft translation that arrives as translated tells the tool the unit is done, so a machine draft that has not been reviewed is better labelled with a value the project's workflow understands.

Inline codes. The content of source and target may hold inline elements for the codes that sit inside the text. The specification names three kinds: elements that hold the native code as content (bpt, ept, it, ph), empty elements that stand in for a code (g, bx, ex, x), and sub for translatable text inside a code. Paired codes are matched by rid values, or by id when no rid exists, and an isolated code carries a mandatory pos attribute of begin or end (XLIFF 1.2 specification). The g element wraps text that may be moved within its parent, which is why a translation may reorder the words inside it, while x and ph stand alone.

The consequence for a translator is mechanical. Every code in the source appears once in the target under its original element name and id; none is dropped, doubled or renumbered; a bpt still has its ept. A rebuilt string with a missing closing code is invalid for the app, and a changed source makes the tool treat the unit as new.

Checks that can fail

Run them against deliberately broken copies, because a check that never turns red on a broken file proves nothing.

  1. Strings.xml. Parse it as XML, then list every placeholder of each source string and of its translation and compare the sets by number and type letter. Remove one apostrophe escape from a copy and confirm your check, or the build, objects.
  2. Plurals. For each target language, compare the categories in the file with the CLDR list for that language. Add an invented category to a copy and see whether the check catches it.
  3. XLIFF. Count target elements per unit (none or one), compare the multiset of inline-code names and ids between each source and target, and confirm the file element has its target-language attribute. Delete one ept from a copy and verify the failure.

Also compare keys: resource names and unit ids are never translated, since code and tools look them up by exact text.

When this does not apply

The Android string resource file and XLIFF as defined in the 1.2 specification are the only formats covered. Version two of XLIFF uses different element names, and other mobile formats such as iOS .strings or .xcstrings, Flutter ARB and web message catalogs have their own rules. The facts come from the Android documentation, the XLIFF 1.2 specification and the CLDR chart as read on 30 October 2026; they may change. We did not run an Android build, so what a particular version of the toolchain does with an unescaped character was not observed, and neither were the handling of resources marked as not translatable and the effect of a missing plural category. The plural examples show three languages out of many, and the rules for others should be taken from the chart.

Tools for the two file types

Both of our skills return the whole file and nothing else. The android-strings-translate skill translates a strings.xml into a target language. It keeps resource names, attributes, comments, placeholders and protected spans as they are, adds the escapes that Android needs, and writes the plural items the target language requires. The xliff-translate skill fills a file of the 1.2 standard: one target after each source of a translatable unit, inline codes with their original names and ids, units marked as not translatable left alone, and the target-language attribute set when it is missing.

Read this post as Markdown: /blog/android-xliff-translation-build-breaks.md · Atom feed.