%@, %1$@, %lld: los especificadores de formato que se rompen al traducir

Actualizado el · LocalePilot

Una cadena de iOS con una variable contiene un %@ o un %lld. El traductor tiene que conservarlo, en el lugar correcto y con el tipo correcto. Los errores que se repiten, uno de ellos propio del francés y del español, y cómo detectarlos.

¿Tienes prisa? Revisa los placeholders de una traducción directamente en el navegador: gratis, sin registro, no se sube nada.

De dónde viene el %

En una app iOS, una cadena localizada con una variable se convierte en una cadena de formato. En SwiftUI ocurre 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 €"

Un Int da %lld, un String da %@, un Double da %lf. Con la API antigua escribes tú el formato: String(format: NSLocalizedString("items_left", comment: ""), count), y el archivo .strings contiene %d o %ld.

En ambos casos, el traductor recibe un texto con signos % en medio, y ahí es donde las cosas se rompen.

Tres formas de romper una traducción

Eliminar el especificador. "Invitez à modifier cette liste" en lugar de "Invitez %@ à modifier cette liste". Nada falla: el argumento simplemente se ignora y el nombre de la persona desaparece de la pantalla. Es el error más frecuente y el más silencioso.

Cambiar el tipo. Sustituir %lld por %@, o al revés. El formato lee entonces el argumento con el tipo equivocado: en el mejor de los casos, un número sin sentido; en el peor, un cierre de la app, porque %@ espera un objeto y recibe un entero.

Cambiar el orden en silencio. "%@ shared %@" se convierte, en un idioma con otro orden de palabras, en una frase en la que los dos nombres están intercambiados. La sintaxis es correcta, el sentido no.

Reordenar con posiciones

Para cambiar el orden, numera los argumentos: %1$@ es el primer argumento, %2$@ el segundo, estén donde estén en la 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$@";

Dentro de una cadena, numera todos los argumentos o ninguno. Mezclar %1$@ y %@ es comportamiento indefinido.

Una trampa del francés (y del español): «20 % de remise»

La tipografía francesa pone un espacio antes del signo de porcentaje: «20 % de remise» (20 % de descuento). La RAE también recomienda ese espacio en español, así que «20 % de descuento» tiene la misma trampa. En una cadena de formato es un problema, porque % d (porcentaje, espacio, d) es un especificador válido: el espacio es un flag de printf («rellenar los números positivos con un espacio») y d la conversión de entero. La frase

"Profitez de %lld % de remise"

contiene por tanto dos especificadores, %lld y % d, para un solo argumento. El segundo lee un valor que no existe. La forma correcta duplica el signo:

"Profitez de %lld %% de remise"

Solo afecta a las cadenas que pasan por un formato, es decir, las que tienen al menos una variable. Un «20 % de remise» fijo, sin argumento, se muestra tal cual. Pero las traducciones evolucionan: el día que alguien añada una variable a esa frase, el % suelto se convierte en un bug.

Plurales

Para «1 artículo / 3 artículos», no escribas dos claves y un if en tu código. En un String Catalog, la clave %lld items left recibe variaciones de plural (one, other, y además few o many según el idioma), cada una con su propio %lld. Cuando una frase tiene dos números que varían de forma independiente, Xcode usa sustituciones (%#@items@): un caso más raro, que se edita en Xcode.

Verificar antes de publicar

La verificación básica es mecánica: para cada traducción, la lista de especificadores (tipo y posición) debe coincidir con la de la fuente. Eso es lo que hace la QA de LocalePilot en los archivos .strings y .xcstrings: reconoce %@, %1$@, %d, %ld, %lld, %.2f, %u, %i y %s, trata %% como texto y señala un especificador faltante o de más antes de exportar. Ignora a propósito las conversiones raras como %e o %x, precisamente para que «20%en» no se tome por un error. El caso «% d» de arriba requiere una revisión humana: busca en tus traducciones al francés o al español un % seguido de un espacio.

Preguntas frecuentes

¿Qué diferencia hay entre %d, %ld y %lld?

El tamaño del entero esperado. SwiftUI usa %lld para un Int interpolado; en un archivo .strings escrito a mano, conserva el especificador de la fuente.

¿Cómo escribo un signo % en una cadena traducida?

En una cadena con variables, duplícalo: %%. Si no, "% d" o "% s" pueden leerse como especificadores.

¿Puede un traductor cambiar el orden de las variables?

Sí, con posiciones: %2$@ y luego %1$@. Pero entonces toda la cadena debe estar numerada.