%@, %1$@, %lld: de format specifiers die breken bij vertalen

Bijgewerkt op · LocalePilot

Een iOS-tekst met een variabele bevat een %@ of een %lld. De vertaler moet die behouden, op de juiste plek en met het juiste type. Hieronder de fouten die steeds terugkomen (één ervan is typisch Frans) en hoe je ze opspoort.

Controleer de placeholders van een vertaling direct in je browser: gratis, zonder account, er wordt niets geüpload.

Waar het % vandaan komt

In een iOS-app wordt een gelokaliseerde tekst met een variabele een format string. In SwiftUI gebeurt dat impliciet:

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 €"

Een Int geeft %lld, een String geeft %@, een Double geeft %lf. Met de oudere API schrijf je het format zelf: String(format: NSLocalizedString("items_left", comment: ""), count), en het .strings-bestand bevat %d of %ld.

Hoe dan ook krijgt de vertaler tekst met %-tekens erin, en daar gaat het mis.

Drie manieren om een vertaling te breken

De specifier laten vallen. "Invitez à modifier cette liste" in plaats van "Invitez %@ à modifier cette liste". Er crasht niets: het argument wordt gewoon genegeerd en de naam van de persoon verdwijnt van het scherm. Het is de meest voorkomende fout en ook de minst zichtbare.

Het type veranderen. %lld vervangen door %@, of omgekeerd. Het format leest het argument dan als het verkeerde type: in het beste geval een onzingetal, in het slechtste een crash, omdat %@ een object verwacht waar het een integer krijgt.

De volgorde stilletjes veranderen. "%@ shared %@" wordt, in een taal met een andere woordvolgorde, een zin waarin de twee namen zijn verwisseld. De syntaxis klopt, de betekenis niet.

Volgorde aanpassen met posities

Om de volgorde te veranderen, nummer je de argumenten: %1$@ is het eerste argument, %2$@ het tweede, waar ze ook in de zin staan.

/* 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$@";

Eén regel om te onthouden: binnen een tekst is alles genummerd of niets. %1$@ en %@ mengen is ongedefinieerd gedrag.

Een Franse valkuil: "20 % de remise"

Breng je een app in het Frans uit, dan is deze het weten waard. De Franse typografie zet een spatie vóór het procentteken: "20 % de remise" (20% korting). In een format string is dat een probleem, want % d (procent, spatie, d) is een geldige specifier: de spatie is een printf-flag ("positieve getallen aanvullen met een spatie") en d de integerconversie. De zin

"Profitez de %lld % de remise"

bevat dus twee specifiers, %lld en % d, voor één argument. De tweede leest een waarde die niet bestaat. De juiste vorm verdubbelt het teken:

"Profitez de %lld %% de remise"

Het geldt alleen voor teksten die door een format gaan, dus teksten met minstens één variabele. Een vaste tekst "20 % de remise" zonder argument wordt weergegeven zoals hij er staat. Maar vertalingen veranderen: zodra iemand een variabele aan die zin toevoegt, wordt het losse % een bug. In het Nederlands schrijf je "20% korting" zonder spatie, maar let op dezelfde valkuil als een vertaler toch een spatie zet.

Meervoudsvormen

Schrijf voor "1 item / 3 items" geen twee sleutels met een if in je code. In een String Catalog krijgt de sleutel %lld items left meervoudsvariaties (one, other, plus few of many afhankelijk van de taal), elk met een eigen %lld. Bevat een zin twee getallen die los van elkaar variëren, dan gebruikt Xcode substituties (%#@items@): een zeldzamer geval, dat je in Xcode bewerkt.

Controleren voordat je uitbrengt

De basiscontrole is mechanisch: voor elke vertaling moet de lijst specifiers (type en positie) overeenkomen met de bron. Dat doet de QA van LocalePilot op .strings- en .xcstrings-bestanden: ze herkent %@, %1$@, %d, %ld, %lld, %.2f, %u, %i en %s, behandelt %% als tekst en meldt een ontbrekende of overbodige specifier vóór de export. Zeldzame conversies zoals %e of %x negeert ze bewust, juist zodat "20%en" niet voor een fout wordt aangezien. Het geval "% d" hierboven moet je zelf controleren: zoek in je Franse vertalingen naar een % gevolgd door een spatie.

Veelgestelde vragen

Wat is het verschil tussen %d, %ld en %lld?

De grootte van de verwachte integer. SwiftUI gebruikt %lld voor een geïnterpoleerde Int; in een handgeschreven .strings-bestand behoud je de specifier van de bron.

Hoe schrijf ik een %-teken in een vertaalde tekst?

In een tekst met variabelen verdubbel je het: %%. Anders kunnen "% d" of "% s" als specifiers worden gelezen.

Mag een vertaler de volgorde van variabelen veranderen?

Ja, met posities: %2$@ en dan %1$@. Maar dan moet de hele tekst genummerd zijn.