It depends on whether the string is a format string. If you pass arguments to it, write the percent sign as %%. If it is never formatted, a plain % is fine, and formatted="false" tells the resource compiler so.
Android only formats a resource when you pass arguments: getString(R.string.discount, 20) and Compose's stringResource(R.string.discount, 20) run the text through String.format(), where % starts a format specifier and %% produces a literal percent sign. getString(R.string.progress_done) without arguments returns the raw text and does not look at % at all.
<!-- Formatted: getString(R.string.discount, 20) gives "Save 20% today" -->
<string name="discount">Save %1$d%% today</string>
<!-- Never formatted: getString(R.string.progress_done) gives "100% done" -->
<string name="progress_done">100% done</string>
The resource compiler checks every translatable string that is not marked formatted="false". When it finds more than one substitution and at least one of them has no position index, it reports multiple substitutions specified in non-positional format; did you mean to add the formatted="false" attribute?. A % followed by a space or a letter counts as a substitution, so text such as "Save 50% on shoes and 70% on hats" triggers it even though it is not meant to be formatted. The Android Gradle plugin passes --legacy to aapt2, so today this is a warning in the build output; the original aapt failed the build on it. For a string that is not a format string, mark it:
<string name="sale" formatted="false">Save 50% on shoes and 70% on hats</string>
For a real format string, fix the string instead: give every placeholder a position (%1$s, %2$d) and write %% for the literal sign. Positions are required as soon as there is more than one placeholder, and they let translators reorder the arguments in languages with a different word order.
Pitfalls:
formatted="false" only silences the compile-time check. If you still call getString(id, args) on that string, String.format() runs and a lone % throws an IllegalFormatException.translatable="false" are not checked either, so a broken format string there only fails at runtime.%% back to %. Check translated files for it before a release.Reference: the Format strings section of the Android string resource guide and the java.util.Formatter syntax.
Published Oct 9, 2026 · Markdown version