%@, %1$@, %lld: os especificadores de formato que quebram na tradução

Atualizado em · LocalePilot

Uma string iOS com variável contém um %@ ou um %lld. Quem traduz precisa mantê-lo, no lugar certo e com o tipo certo. Os erros que sempre voltam, um deles específico do francês, e como identificá-los.

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.

Perguntas frequentes

Qual a diferença entre %d, %ld e %lld?

O tamanho do inteiro esperado. O SwiftUI usa %lld para um Int interpolado; em um arquivo .strings escrito à mão, mantenha o especificador da origem.

Como escrever o sinal % em uma string traduzida?

Em uma string com variáveis, duplique-o: %%. Caso contrário, "% d" ou "% s" podem ser lidos como especificadores.

Quem traduz pode mudar a ordem das variáveis?

Sim, com posições: %2$@ e depois %1$@. Mas aí a string inteira precisa ser numerada.