Com pressa? Verifique os placeholders de uma tradução direto no navegador: grátis, sem cadastro, nada é enviado.
De onde vem o %
Em um app iOS, uma string localizada com variável vira uma string de formato. No SwiftUI isso acontece de forma implícita:
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 €"
Um Int gera %lld, uma String gera %@, um Double gera %lf. Com a API mais antiga, você mesmo escreve o formato: String(format: NSLocalizedString("items_left", comment: ""), count), e o arquivo .strings contém %d ou %ld.
De um jeito ou de outro, quem traduz recebe um texto com sinais de % no meio, e é aí que as coisas quebram.
Três jeitos de quebrar uma tradução
Perder o especificador. "Invitez à modifier cette liste" em vez de "Invitez %@ à modifier cette liste". Nada trava: o argumento é simplesmente ignorado e o nome da pessoa some da tela. É o erro mais comum e o mais silencioso.
Trocar o tipo. Substituir %lld por %@, ou o contrário. O formato passa a ler o argumento com o tipo errado: na melhor das hipóteses um número sem sentido, na pior um crash, já que %@ espera um objeto e recebe um inteiro.
Mudar a ordem em silêncio. "%@ shared %@" vira, em um idioma com outra ordem de palavras, uma frase em que os dois nomes estão trocados, com uma sintaxe que continua válida.
Reordenar com posições
Para mudar a ordem, numere os argumentos: %1$@ é o primeiro argumento, %2$@ o segundo, onde quer que estejam na frase.
/* 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$@";
Uma regra para guardar: dentro de uma string, ou tudo é numerado, ou nada é. Misturar %1$@ e %@ é comportamento indefinido.
Uma armadilha francesa: "20 % de remise"
A tipografia francesa coloca um espaço antes do sinal de porcentagem: "20 % de remise" (20% de desconto). Em uma string de formato isso é um problema, porque % d (porcentagem, espaço, d) é um especificador válido: o espaço é uma flag do printf ("preencher números positivos com um espaço") e o d é a conversão de inteiro. A frase
"Profitez de %lld % de remise"
contém, portanto, dois especificadores, %lld e % d, para um único argumento. O segundo lê um valor que não existe. A forma correta duplica o sinal:
"Profitez de %lld %% de remise"
Isso só afeta strings que passam por um formato, ou seja, strings com pelo menos uma variável. Um "20 % de remise" fixo, sem argumento, aparece como está escrito. Mas as traduções evoluem: no dia em que alguém adicionar uma variável a essa frase, o % isolado vira um bug. Em português o risco é menor, já que escrevemos "20% de desconto" sem espaço, mas o sinal ainda precisa ser duplicado (%lld%% de desconto) em uma string com variáveis.
Plurais
Para "1 item / 3 itens", não escreva duas chaves e um if no seu código. Em um String Catalog, a chave %lld items left recebe variações de plural (one, other, além de few ou many conforme o idioma), cada uma com o próprio %lld. Quando uma frase tem dois números que variam de forma independente, o Xcode usa substituições (%#@items@): um caso mais raro, editado no Xcode.
Verificar antes de publicar
A verificação básica é mecânica: para cada tradução, a lista de especificadores (tipo e posição) precisa bater com a da origem. É o que a QA do LocalePilot faz nos arquivos .strings e .xcstrings: ela reconhece %@, %1$@, %d, %ld, %lld, %.2f, %u, %i e %s, trata %% como texto e aponta um especificador ausente ou a mais antes da exportação. Ela ignora de propósito conversões raras como %e ou %x, justamente para que "20%en" não seja tomado por um erro. O caso "% d" acima fica para a revisão humana: procure nas suas traduções em francês um % seguido de espaço.