Skip to main content

Android substitutes the wrong value in a translated string

A translated strings.xml swaps, drops or invents a positional format argument, so getString fills the wrong value or throws. How it happens and how to check every values-* directory.

Android XML

What you see

  • A string shows the wrong value in one language: a date where a name belongs, or a number that belongs elsewhere in the sentence.
  • Or the app throws `IllegalFormatException` / `MissingFormatArgumentException` for users in that locale only.
  • Lint passes, because each `values-*/strings.xml` is well formed on its own.

Why it happens

Android substitutes positionally. `%1$s` is the first argument, `%2$s` the second, and a translation is free to reorder them for its own grammar — that is what the explicit indices are for. What breaks is a translation that changes the index without meaning to, drops one, or adds an index the call site never supplies. Since each file is valid XML, nothing local can tell.

1Translations written against an older version of the source

A format string changes in one branch while its translations are produced in another. Both merge cleanly; the arguments no longer line up. This is the most common cause and the hardest to see in review, because each side looks correct.

ACINQ/phoenix1.1k★

Seven locales carried format-argument mismatches. The maintainer named the cause when merging: an lnurl change and the Italian, Polish, Japanese, Korean and Ukrainian localisations were worked in parallel, and the translations were done against older files and never reconciled.

Our report — merged 2026-09-23 →

2An index is dropped or invented

A translation that omits `%2$s` leaves an argument unused, which is survivable; one that references `%3$s` against a two-argument call throws at format time, which is not.

ACINQ/phoenix1.1k★

Five locales dropped an argument and two added one the call site does not pass.

Our report — included in the merged fix →

Find every instance

Point the checker at your resources directory; it reads every `values-*/strings.xml` as a locale:

npx @shipi18n/cli@latest check ./app/src/main/res -s en --severity 'missing-key=off,untranslated=off,orphan-key=off'

Reordering positional arguments is legal and is not reported. Dropping one, or referencing an index the source does not define, is. The Android grammar also knows the escaping rules for apostrophes and quotes.

✗ it  coverage 100.0%  2 error(s), 0 warning(s)
    error  lnurl_withdraw_confirm  placeholder-missing — dropped %2$s

How to fix it

  1. 1Match the argument set to the source, keeping whatever order the target language needs. Explicit indices exist precisely so a translation can reorder safely.
  2. 2When a format string changes, re-check every locale rather than the one that reported a crash.
  3. 3Run the check in CI, which is what catches the parallel-branch case: the source change and the stale translations merge separately and only conflict at runtime.

The rules that catch this

How often this actually happens

Android and Apple projects were the last formats our scan learned to read, and both turned up real breakage immediately: https://shipi18n.com/oss