wandres.dev
LOCALIZACIÓN · idiomas y regiones

Plurales y variaciones: por qué una frase no se construye sumando trozos

El plural no es una regla, es una función de la lengua que en árabe tiene seis casos, en polaco tres y en japonés uno solo. Esta lección desarma la intuición de que basta con añadir una ese, explica las categorías de plural del estándar CLDR, muestra cómo se declaran variaciones por cantidad y por dispositivo dentro de un String Catalog, y argumenta por qué concatenar fragmentos traducidos produce interfaces que no se pueden arreglar en ningún idioma flexivo.

⏱ 18 min

Hay un momento, en todo proyecto que se internacionaliza, en que alguien escribe una condición que compara la cantidad con uno y elige entre dos textos. Ese código funciona en inglés, funciona en español y crea una deuda que solo se paga cuando el producto entra en un mercado eslavo o árabe, momento en el que resulta que la elección no era binaria y que el punto de decisión está mal colocado: no es el código quien debe elegir la forma de la frase, sino la lengua de destino, que es la única que conoce sus propias reglas. La localización madura invierte esa relación por completo. El programa deja de construir frases y pasa a aportar dos cosas, un identificador y unos valores, delegando en el catálogo y en Foundation la decisión de qué forma toma la oración. Es la misma renuncia al control que uno hace al dejar de formatear fechas a mano, y produce el mismo alivio.

🎯 Al terminar esta lección sabrás
  • Enumerar las categorías de plural del estándar CLDR y explicar por qué no coinciden entre idiomas.
  • Declarar variaciones por cantidad y sustituciones de varias variables dentro de un String Catalog.
  • Usar variaciones por dispositivo para diferenciar vocabulario de interacción sin duplicar cadenas.
  • Reconocer y eliminar la concatenación de fragmentos traducidos y las falsas plantillas.

La aritmética de los plurales

El consorcio Unicode publica en el repositorio CLDR las reglas de plural de cada lengua, y define seis categorías posibles: cero, uno, dos, pocos, muchos y otro. Ninguna lengua usa las seis con el mismo criterio y casi ninguna usa todas. El inglés y el español distinguen dos, uno y otro. El japonés, el chino y el coreano distinguen una sola: la forma no cambia con la cantidad. El ruso y el polaco distinguen tres, con una regla que depende del último dígito y del penúltimo a la vez. El árabe usa las seis. Y el galés, que sirve de recordatorio de que la intuición no basta, distingue formas para uno, dos, tres y seis.

Dos matices desmontan la simplificación más común, que consiste en suponer que la categoría de uno se aplica al número uno. En francés, la forma singular cubre también el cero. En ruso, veintiuno toma la forma de uno y ciento once toma la de muchos. Y en varios idiomas la categoría depende de si el número tiene decimales, de modo que uno coma cero no se comporta como uno. La conclusión operativa es que ninguna función escrita en el lado del programa puede decidir correctamente sin reimplementar CLDR, y reimplementar CLDR es exactamente lo que no hay que hacer.

⚠️
La categoría de cero casi nunca es la que quieres

Que exista una categoría de cero en árabe o en letón no significa que sea el lugar donde poner la frase de estado vacío. Esa categoría es una forma gramatical de la lengua, no un caso de interfaz. Si quieres decir que no hay elementos con una redacción distinta y más amable, eso es un caso concreto del catálogo, no una regla de plural, y conviene declararlo como tal para que las lenguas que carecen de esa categoría también lo reciban.

Hay una segunda familia de reglas que casi nadie recuerda y que el mismo estándar define aparte: la de los ordinales. Decir el tercer intento o la segunda planta no usa las categorías del plural cardinal sino un conjunto propio, y el inglés es el ejemplo más visible porque necesita cuatro terminaciones distintas según el último dígito. Componer un ordinal pegando un sufijo al número funciona en un idioma y produce disparates en el resto.

let posicion = IntegerFormatStyle<Int>().notation(.compactName)
let ordinal = NumberFormatter(); ordinal.numberStyle = .ordinal

Del lado del código, todo esto se reduce a no decidir. Se declara una clave, se interpola la cantidad y se deja que la tabla compilada resuelva la forma con el idioma activo.

Text("\(pendientes) tareas pendientes")

let resumen = String(localized: "bandeja.no.leidos.\(sinLeer)",
                     defaultValue: "\(sinLeer) mensajes sin leer",
                     comment: "Resumen de la bandeja, cantidad variable")

En el editor del catálogo, cualquier cadena con un marcador numérico puede convertirse en variación por plural, y Xcode genera entonces las categorías que ese idioma concreto necesita, no las seis. Ahí está la elegancia del modelo: el español muestra dos casillas, el ruso tres y el árabe seis, sobre la misma clave y sin que el código lo sepa. Al compilar, esas variaciones se convierten en las entradas de .stringsdict que ya existían antes, con la diferencia de que nadie ha tenido que escribir XML a mano.

flowchart TB
a[Una clave con marcador numerico] --> b[Variacion por plural en el catalogo]
b --> c[Espanol: uno y otro]
b --> d[Ruso: uno, pocos y muchos]
b --> e[Arabe: seis categorias]
b --> f[Japones: una sola forma]
c --> g[Foundation elige la forma con la cantidad y el idioma]
d --> g
e --> g
f --> g

Cuando la frase contiene dos cantidades, la variación simple no basta, porque cada una tiene su propia regla y la combinación no es un producto que se pueda escribir en una sola dimensión. Para eso existen las sustituciones: se nombra cada marcador y se le asocia su propia variación por plural, de modo que la frase queda como una plantilla con dos huecos independientes y cada hueco resuelve su forma por separado.

// Dos cantidades independientes: cada una necesita su propia variacion
Text("\(archivos) archivos en \(carpetas) carpetas")

La otra variación disponible es por dispositivo, y su uso correcto es más estrecho de lo que parece. Sirve para el vocabulario de interacción, que cambia con el hardware aunque el significado sea idéntico: se toca en un teléfono, se hace clic en un ordenador, se mira y se pulsa en unas gafas. Declarar esa diferencia como variación evita el patrón que aparece siempre en su ausencia, que consiste en tener dos claves casi iguales y descubrir seis meses después que solo una de ellas se tradujo.

📱

Por dispositivo

Toca, haz clic, pulsa la corona. La misma clave con vocabulario distinto por familia de producto, sin duplicar la entrada.

🔢

Por plural

Las categorías que ese idioma necesita, generadas por el editor. El código nunca elige la forma.

🧩

Sustituciones

Marcadores con nombre para frases con varias cantidades, cada uno con su regla independiente.

Conviene además saber qué ocurre por debajo, porque explica un par de comportamientos desconcertantes. Al compilar, cada variación se convierte en una entrada de .stringsdict con su regla de plural y su especificador de formato, y ese especificador debe coincidir con el tipo del argumento: un entero de sesenta y cuatro bits usa %lld y un decimal usa otro distinto. Cuando alguien edita una variación a mano y desajusta ese detalle, el resultado no es un error de compilación sino una cadena que muestra el marcador en crudo o un número absurdo, y el rastro hasta la causa es largo.

Hay un caso que conviene resolver aparte y que se confunde a menudo con el plural: el número muy grande. Una frase que dice el recuento exacto de mensajes deja de ser informativa a partir de cierto umbral y empieza a estorbar en el diseño. La solución no es una categoría de plural sino una decisión de producto, aplicada al valor antes de interpolarlo, y Foundation la ofrece con estilos de formato abreviado que respetan la convención de cada región.

Por qué no se concatenan frases

La tentación de componer una oración a partir de trozos traducidos aparece en cuanto hay una parte variable, y es la fuente más productiva de textos imposibles. El motivo no es estético, es estructural, y conviene enunciarlo con precisión: la traducción no es un homomorfismo. La traducción de una frase no equivale a la concatenación de las traducciones de sus partes, porque la forma de cada parte depende del resto de la oración y porque el orden de los elementos no se conserva.

// Mal: tres cadenas que ninguna lengua flexiva puede recomponer
let texto = traducir("Se han eliminado ") + "\(n)" + traducir(" elementos de ") + carpeta

// Bien: una sola frase con huecos, traducible como unidad
let texto = String(localized: "papelera.eliminados.\(n).de.\(carpeta)",
                   defaultValue: "Se han eliminado \(n) elementos de \(carpeta)",
                   comment: "Confirmación tras vaciar elementos de una carpeta")

Los fallos que produce el primer enfoque son de cuatro tipos y ninguno se arregla desde el código. El orden de las palabras cambia: en japonés el verbo va al final y el fragmento intermedio queda descolocado. La concordancia se rompe: en alemán el artículo depende del género del sustantivo que aparece en la variable, y quien tradujo el fragmento fijo no podía saberlo. La declinación cambia la forma del sustantivo según su función en la oración, de modo que la palabra insertada tendría que aparecer flexionada. Y el espaciado y la puntuación siguen convenciones distintas, con el francés exigiendo espacio fino antes de ciertos signos.

El caso que más resistencia genera es el del texto con formato mixto, donde una parte va en negrita o es un enlace y por eso alguien la separa. No hace falta separarla: la cadena admite marcas de estilo ligero que sobreviven a la traducción y que se interpretan al construir el texto, de modo que quien traduce ve una frase completa y puede mover el fragmento resaltado adonde su lengua lo pida.

// El énfasis viaja dentro de la frase y quien traduce puede reubicarlo
Text(try! AttributedString(markdown: String(localized: "Se guardó en **\(carpeta)**")))

Existe una variante más sutil del mismo error que sobrevive a muchas revisiones: la plantilla que parece una frase pero es una lista de fragmentos. Un texto que dice inicio de la acción, luego un nombre, luego una preposición y luego una fecha, aunque viaje en una sola cadena, obliga a quien traduce a respetar un orden que su lengua no admite. La prueba a la que hay que someter toda cadena es simple de formular: si quien traduce no puede reordenar libremente los marcadores dentro de la frase, la cadena está mal diseñada.

Concordancia automática y comprobación

La plataforma ofrece una herramienta que reduce mucho el problema de la concordancia cuando la variable es un sustantivo del propio catálogo: el motor de flexión automática. Se marca el fragmento que debe concordar y el sistema ajusta artículo, género y número según el término insertado, usando los datos morfológicos de la lengua.

Text("^[\(cantidad) mensaje](inflect: true) sin leer")

Conviene conocer sus límites antes de apoyarse en él. Funciona en el conjunto de idiomas para los que Apple ha publicado datos morfológicos, no en todos; cubre la concordancia gramatical, no la reescritura de la oración; y no sustituye a las variaciones por plural, sino que se combina con ellas. Tomarlo como una solución universal produce un tipo de defecto especialmente molesto, que es el texto correcto en cinco idiomas y sutilmente agramatical en los otros siete, donde nadie del equipo puede detectarlo.

Queda una decisión de diseño que la flexión no resuelve y que conviene tomar de forma explícita: el género de la persona usuaria. Muchas lenguas exigen concordar el adjetivo o el participio con quien recibe el mensaje, y si tu producto no conoce ese dato, la única salida honesta es redactar de forma neutra, cosa que hay que pedir por escrito a quien traduce porque no lo adivinará. Escribir la frase con un participio en masculino y confiar en que pase desapercibido es una decisión, aunque nadie la haya tomado conscientemente.

La comprobación merece la misma seriedad que el resto. Un plural mal declarado no rompe la compilación ni lanza una excepción: produce una frase gramaticalmente incorrecta que solo detecta alguien que hable la lengua, lo que la convierte en la clase de defecto que más tarda en descubrirse. La disciplina que funciona es probar los valores frontera de cada familia de reglas en lugar de probar un valor cualquiera.

💡
Los valores que hay que probar siempre

Cero, uno, dos, cinco, once, veintiuno, ciento uno y un decimal. Ese conjunto pequeño activa categorías distintas en las familias eslava, árabe y celta, y por tanto revela con muy poco esfuerzo si una variación está incompleta. Añadirlos como casos de una vista de previsualización con varios idiomas fijados cuesta diez minutos y cubre la clase entera de defectos.

Una interfaz no es un texto con huecos: es una función de sus datos hacia una lengua

El error de concatenar frases no es un descuido de implementación, es un modelo mental equivocado sobre qué clase de objeto es un mensaje de interfaz, y merece desmontarse en sus propios términos porque el mismo modelo equivocado reaparece en la generación de voz, en las notificaciones y en cualquier salida dirigida a personas. Quien concatena está tratando la frase como una estructura sintáctica cuyos nodos se pueden traducir de forma independiente, es decir, está asumiendo que traducir es un homomorfismo sobre el monoide libre de las cadenas: que traducir de la concatenación equivale a la concatenación de las traducciones. Esa suposición es falsa en toda lengua natural, y es falsa de una manera profunda y no reparable, porque la forma de cada palabra depende de su función sintáctica dentro de la oración completa, y esa función no está presente en el fragmento. Un fragmento aislado carece de la información necesaria para traducirse; no es que se traduzca mal, es que la operación no está definida sobre él. La formulación correcta es la contraria y es la que adoptan todos los sistemas serios de localización: el mensaje se declara como una función total desde una tupla de datos hacia una cadena de la lengua de destino, y esa función se implementa una vez por idioma por alguien que conoce esa lengua. El programa aporta el dominio, es decir, el identificador del mensaje y sus argumentos con su tipo y su significado; la lengua aporta la implementación. Bajo ese modelo las variaciones por plural, las sustituciones y la flexión automática dejan de parecer características sueltas del catálogo y se revelan como lo que son: los mecanismos por los que esa función puede depender de sus argumentos de una forma que el programa no necesita conocer. Y se sigue una regla de diseño verificable, que además es fácil de aplicar en una revisión de código: cada frase que ve una persona debe corresponder a exactamente una clave, y todo lo que varíe dentro de ella debe entrar como argumento con nombre, nunca como trozo de cadena. Si al leer una línea no puedes nombrar la tupla de entrada, la frase todavía no está internacionalizada.

📝
Lo esencial

El estándar CLDR define seis categorías de plural y cada lengua usa las suyas: una en japonés, dos en español, tres en ruso, seis en árabe. El código no debe elegir la forma nunca; declara la clave e interpola la cantidad. En el catálogo, la variación por plural genera solo las categorías del idioma, las sustituciones resuelven frases con varias cantidades y la variación por dispositivo evita duplicar claves. Concatenar fragmentos traducidos rompe orden, concordancia y declinación de forma irreparable. Y hay que probar cero, uno, dos, cinco, once y veintiuno.

⚔️ Romper tus frases a propósito
  1. Busca en tu código toda condición que compare una cantidad con uno para elegir texto y sustitúyela por una variación por plural en el catálogo.
  2. Localiza las frases construidas por concatenación y reescribe cada una como una sola clave con marcadores; anota cuántas tenías.
  3. Añade ruso o árabe al proyecto y comprueba cuántas casillas de plural aparecen vacías en cadenas que creías completas.
  4. Crea una previsualización que recorra los valores cero, uno, dos, cinco, once y veintiuno en tres idiomas de familias distintas.
  5. Aplica flexión automática a un mensaje con sustantivo variable y verifica en qué idiomas de tu lista tiene efecto y en cuáles no.