%@, %1$@, %lld: the format specifiers that break in translation
Updated · GLabs325
An iOS string with a variable contains a %@ or a %lld. The translator has to keep it, in the right place and with the right type. The mistakes that keep coming back, one of them specific to French, and how to spot them.
Where the % comes from
In an iOS app, a localized string with a variable becomes a format string. In SwiftUI it happens implicitly:
Text("\(count) items left") // key: "%lld items left"
Text("Welcome back, \(name)!") // key: "Welcome back, %@!"
String(localized: "Total: \(price) €") // price is a Double: "Total: %lf €"
An Int gives %lld, a String gives %@, a Double gives %lf. With the older API you write the format yourself: String(format: NSLocalizedString("items_left", comment: ""), count), and the .strings file holds %d or %ld.
Either way, the translator gets text with % signs in the middle, and that's where things break.
Three ways to break a translation
Dropping the specifier. "Invitez à modifier cette liste" instead of "Invitez %@ à modifier cette liste". Nothing crashes: the argument is simply ignored and the person's name vanishes from the screen. It's the most common mistake and the quietest.
Changing the type. Replacing %lld with %@, or the other way round. The format then reads the argument as the wrong type: at best a nonsense number, at worst a crash, since %@ expects an object where it gets an integer.
Changing the order silently. "%@ shared %@" turns, in a language with a different word order, into a sentence where the two names are swapped. The syntax is fine, the meaning isn't.
Reordering with positions
To change the order, number the arguments: %1$@ is the first argument, %2$@ the second, wherever they sit in the sentence.
/* source */ "%@ shared %@ with you" = "%1$@ shared %2$@ with you";
/* French */ "%@ shared %@ with you" = "%1$@ vous a partagé %2$@";
/* other order */ "%@ shared %@ with you" = "%2$@ : partagé par %1$@";
One rule to remember: within a string, either everything is numbered or nothing is. Mixing %1$@ and %@ is undefined behavior.
A French trap: "20 % de remise"
If you ship in French, this one is worth knowing. French typography puts a space before the percent sign: "20 % de remise" (20% off). In a format string that's a problem, because % d (percent, space, d) is a valid specifier: the space is a printf flag ("pad positive numbers with a space") and d the integer conversion. The sentence
"Profitez de %lld % de remise"
therefore holds two specifiers, %lld and % d, for a single argument. The second one reads a value that doesn't exist. The correct form doubles the sign:
"Profitez de %lld %% de remise"
It only affects strings that go through a format, meaning strings with at least one variable. A fixed "20 % de remise" with no argument displays as written. But translations evolve: the day someone adds a variable to that sentence, the lone % becomes a bug.
Plurals
For "1 item / 3 items", don't write two keys and an if in your code. In a String Catalog, the %lld items left key gets plural variations (one, other, plus few or many depending on the language), each with its own %lld. When a sentence has two numbers that vary independently, Xcode uses substitutions (%#@items@): a rarer case, edited in Xcode.
Checking before you ship
The basic check is mechanical: for each translation, the list of specifiers (type and position) must match the source. That's what LocalePilot's QA does on .strings and .xcstrings files: it recognizes %@, %1$@, %d, %ld, %lld, %.2f, %u, %i and %s, treats %% as text, and flags a missing or extra specifier before export. It deliberately ignores rare conversions such as %e or %x, precisely so that "20%en" isn't taken for an error. The "% d" case above is one for human review: search your French translations for a % followed by a space.
Frequently asked questions
What's the difference between %d, %ld and %lld?
The size of the expected integer. SwiftUI uses %lld for an interpolated Int; in a hand-written .strings file, keep the source's specifier.
How do I write a % sign in a translated string?
In a string with variables, double it: %%. Otherwise "% d" or "% s" may be read as specifiers.
Can a translator change the order of variables?
Yes, with positions: %2$@ then %1$@. But then the whole string must be numbered.