Concordancia de número: {plural <count>: forma1|forma2|forma3}

El ruso — y toda lengua eslava — exige que el sustantivo concuerde con el número cardinal que lo precede: 1 язык, 2 языка, 5 языков. La elección depende de la cantidad módulo 100 y 10, con excepciones para 11–14. Spintax es el primer motor de la familia spintax que lo trae como primitiva de primera clase. Antes, cada redacción reinventaba la regla en las plantillas y fallaba en algún punto — o evitaba en silencio cualquier construcción con números.

La sintaxis

{plural <count>: form1|form2|form3}

El prefijo literal {plural (con un espacio al final) es el discriminante inequívoco frente a la forma de sinónimo {a|b|c}. Los : separan el hueco de la cantidad del de las formas. Las formas van separadas por barra vertical.

Familia de localesIdiomasFormas
Eslavo oriental ru, uk, be 3: one|few|many
BCS sr, hr, bs 3: one|few|many
Estilo EN (por defecto) en, es, pt, de, it, fr, nl, sv, no, da, fi, … 2: one|many

El serbio, el croata y el bosnio reutilizan la regla de cubos del eslavo oriental carácter por carácter — one para 1, 21, 101 (pero no 11), few para 2–4, 22–24 (pero no 12–14), many para todo lo demás, incluido el cero. CLDR llama other a ese tercer cubo en BCS en vez de many; por posición es el mismo hueco, así que una plantilla escrita con la aridad rusa funciona sin cambios.

El árabe, el galés, el hebreo y el letón tienen estructuras de cubos distintas y a propósito no están implementados. Irán entrando idioma a idioma, según aparezca demanda real.

Ejemplos

supports %LangCount% {plural %LangCount%: language|languages}
ships with %IntegrationCount% {plural %IntegrationCount%: integration|integrations}
processed in {plural %PayoutHours%: hour|hours}

поддерживает %LangCount% {plural %LangCount%: язык|языка|языков}
получите %BonusCount% {plural %BonusCount%: бонус|бонуса|бонусов}
завершить за {plural 30: день|дня|дней}

El hueco de la cantidad admite una referencia %Var% o un entero literal. Cuando corre la pasada de plural, la sustitución de variables ya ocurrió — así que el ayudante solo ve ahí una cadena entera, sin importar cuál de las dos formas escribió la redacción.

La regla del locale (RU, 3 cubos)

La regla rusa es famosa por quisquillosa. La tabla completa:

Cantidad nCuboEjemplo RU
1, 21, 31, 41, …, 101, 121one1 язык, 21 язык
2–4, 22–24, 32–34, …few2 языка, 23 языка
0, 5–20, 25–30, 35–40, …, 100, 111–114many0 языков, 11 языков, 25 языков

Las excepciones de 11–14 (que por sus últimos dígitos parecerían one y few) tumban los apaños. El ayudante del motor cierra ese hueco de una vez para toda redacción, todo contador y toda plantilla. Algoritmo:

const abs = Math.abs(n);
const mod10 = abs % 10;
const mod100 = abs % 100;

if (mod10 === 1 && mod100 !== 11) return forms[0];                                  // one
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return forms[1];     // few
return forms[2];                                                                    // many

Los números negativos pasan por abs, igual que CLDR. El cero toma la forma many («0 языков») porque así se escribe correctamente en ruso, no porque el cero sea un caso especial.

Por qué con dos puntos (y no {plural %N%|formas})

Un boceto anterior escribía {plural %LangCount%|язык|языка|языков}, solo con barras. Dos problemas estructurales acabaron con esa forma:

1. El peligro de la variable ayudante. Una macro habitual de preajuste:

#set %LangPlural% = {plural %LangCount%: язык|языка|языков}

Si la construcción quedara en {12|язык|языка|языков} tras la sustitución, sería indistinguible de un sinónimo de cuatro opciones — y la siguiente etapa de la tubería elegiría una al azar tan contenta. La forma con dos puntos conserva el prefijo discriminante a través de la expansión, así que la pasada de plural puede correr sin riesgo después de la sustitución de variables.

2. Los enteros literales. {30|день|дня|дней} choca con la forma de sinónimo {a|b|c} — el analizador no puede distinguirlas. La forma con dos puntos hace {plural 30: день|дня|дней} estructuralmente distinta.

Casos numéricos límite

El hueco de la cantidad se analiza con rigor. Si un valor fuese a significar en silencio algo distinto de lo que la redacción espera, la construcción se resuelve a cadena vacía.

Hueco de la cantidadResultadoPor qué
12forma elegidaentero simple
-3forma elegida para 3abs(), como CLDR
0forma elegida (RU: many; EN: many)el cero es gramatical
12 forma elegida para 12se recortan los espacios
(vacío)construcción entera → vacíafalta la cantidad
%MissingVar% (no sustituida)construcción entera → vacíano es número tras la expansión
1,200construcción entera → vacíala coma no es dígito; parseInt mentiría y devolvería 1
12abc / 08hconstrucción entera → vacíase rechazan los caracteres no numéricos finales
1.5construcción entera → vacíasolo enteros en la v1

Si quieres borrar la frase entera cuando falta la cantidad (y no solo la construcción), protege la frase con un condicional:

{?HasLanguages?supports %LangCount% {plural %LangCount%: language|languages}|}

Hueco de las formas — sin corchetes spintax anidados

Las formas deben ser texto plano. No pueden contener corchetes spintax anidados { } [ ]. Una forma como {plural 1: {a|b}|c} se rechaza, y también {plural 1: [<and>day|days]}. El validador lo reporta como error; la ejecución es tolerante y degrada el bloque a llaves de ancho completo en vez de lanzar (ver abajo).

Si de verdad necesitas contenido condicional o aleatorio dentro de una forma, sácalo antes a una variable — y tiene que ser #def, no #set:

/# wrong: nested synonym in form #/
{plural 2: {integration|connector}|integrations}

/# also wrong: #set is a macro — the brackets come straight back #/
#set %Noun% = {integration|connector}
{plural 2: %Noun%|%Noun%s}
→  {plural 2: {integration|connector}|{integration|connector}s}

/# right: #def resolves once, so the form slot receives plain text #/
#def %Count% = 2
#def %Brand% = {Acme|Acme Cloud}
{plural %Count%: %Brand% integration|%Brand% integrations}
→  Acme Cloud integrations

Esa es la distinción que hace valiosa a #def. #set sustituye su valor tal cual en cada referencia, así que sacar un sinónimo a un #set devuelve los corchetes justo al hueco que querías dejar limpio. #def tira su valor una vez por render y entrega al hueco de forma el texto ya resuelto.

Fíjate en qué se extrae: un fragmento que no flexiona, igual en todas las formas. Es la única figura que admite este patrón. No saques un sustantivo para construir sus formas concatenando sufijos a la variable — %Noun%а funciona con un sinónimo y produce basura con el siguiente. Cuando la palabra misma flexiona, escribe las formas completas; para eso están los huecos de forma.

Las etiquetas HTML (<em>, <a href="…">) y las %Var% sin resolver sobreviven sin daño en el texto de las formas — solo se prohíben los corchetes estructurales de spintax.

Ejecución tolerante — una construcción rota no tumba la página

Si un corchete se cuela en el hueco de forma, o el número de formas no cuadra con la aridad del locale, el motor captura el error bloque a bloque y emite la construcción tal cual, con llaves de ancho completo (U+FF5B / U+FF5D):

supports 5 {plural 5: язык|языка}

Las llaves de ancho completo se parecen casi a las {} ASCII pero son puntos de código distintos — atraviesan las siguientes etapas de la tubería sin que el resolutor de enumeraciones las malinterprete. La página se renderiza, el fallo queda visible en el HTML y operaciones lo arregla sin un 500.

El validador (y el playground) corre en cambio en modo estricto: ambas clases de error lanzan, con un campo position y el texto literal de la construcción. La redacción pilla el fallo antes de que la plantilla llegue a producción.

De dónde sale el locale

Un locale por llamada de render. Sin override por construcción en la v1.

  • Plugin de WordPress: gana el post meta por plantilla _spintax_locale; si no, el locale del sitio WordPress (get_locale()).
  • @spintax/core (independiente): el campo locale de render(tpl, { locale }). La fuente la decide el anfitrión — cabecera de la petición, preferencia del usuario, configuración del sitio. Omítelo y te quedas con el valor por defecto de 2 formas.
  • Playground: sigue el idioma de la página — inglés en /play/EN, ruso en /ru/play/. No hay selector de locale dentro de la página; cambia de página con el selector de idioma de la navegación.

La cadena de locale se normaliza a su etiqueta base — ru-RUru, uk_UAuk, pt-BRpt. La tabla de aridad se consulta por etiqueta base. Las subetiquetas de escritura y región no llevan gramática de plural, así que sr-Latn, sr-Cyrl, sr_RS y sr-Latn-RS se normalizan todas a sr y reciben las mismas tres formas. Las etiquetas de tres letras no se mapean: srp sigue siendo srp y cae en el valor por defecto de 2 formas.

Lugar en la tubería

1. strip comments
2. extract #set directives
3. apply conditionals          (pass 1)
4. expand %var% references
5. apply conditionals          (pass 2)
6. apply plurals               ← this stage
7. resolve enumerations
8. resolve permutations
9. post-process

La pasada de plural corre después de la expansión de variables (para que %LangCount% ya sea una cadena entera en el hueco de la cantidad) y antes de la resolución de enumeraciones (para que el resolutor de sinónimos nunca tenga ocasión de malinterpretar una construcción mal formada).

Ejemplo trabajado: comparativa de productos

Tres filas de un catálogo comparativo de SaaS. La misma plantilla renderiza cada una, pero la cantidad manda tanto en la forma del sustantivo como — de rebote — en lo concreto que suena el texto.

ProductoIdiomasIntegracionesPlanes
Acme16183
Beta115
Gamma12172

Sin primitiva de plural, cada producto renderiza la misma frase vaga: «supports many integrations, including Slack, GitHub, Linear». Las diferencias del catálogo quedan invisibles. Con la primitiva, la plantilla puede decir:

supports %IntegrationCount% {plural %IntegrationCount%: integration|integrations},
including %TopIntegrations%

Renderiza, por fila:

  • Acme: supports 18 integrations, including Slack, GitHub, Linear
  • Beta: supports 1 integration, including Slack
  • Gamma: supports 17 integrations, including Slack, GitHub, Linear

La diferencia factual está ahora en el texto. El SEO gana con diferenciación auténtica; quien lee gana números concretos en lugar de «muchas».

Antipatrones

1. Emparejamiento en línea de conjunto cerrado

El apaño que «funciona por accidente»:

{50|100|150|200} баллов

Cada número elegido pide por casualidad la forma many, así que el sustantivo nunca desentona. Se rompe en cuanto el número viene de una variable real — cualquier valor entre 21–24 o 31–34 caerá con la forma equivocada.

2. Condicionales con banderas de cubo

El apaño del «ingeniero en el bucle»:

%LangCount% {?HasOneLang?language|{?HasFewLangs?languages|languages}}

Tres banderas booleanas por entidad contable en el ensamblador de variables, y condicionales anidados en cada plantilla. La redacción no puede escribir una construcción %count% %noun% nueva sin pedir antes a un ingeniero que añada el trío de banderas y publique un build. Ese es justo el flujo que la primitiva vino a eliminar.

3. Envoltorios de lista en vez de cantidades

El apaño de la «evitación silenciosa»:

supports many integrations, such as %TopIntegrations%

La redacción rodea la cantidad porque la herramienta no sabe expresarla. Resultado: todas las entradas se leen igual, sin diferenciación SEO y sin autoridad editorial. Enseña la cantidad.

Contexto del sector

La concordancia de número es primitiva de primera clase en cualquier pila de i18n: ICU MessageFormat ({count, plural, one {…} few {…} other {…}}), ngettext de gettext, FormatJS, etc. Pertenece a esa categoría de primitivas gramaticales universales sin las que ningún sistema de contenido serio se sostiene.

Qué hicimos distinto: la convertimos en una primitiva nativa de spintax. ICU exige otra sintaxis de plantilla, lo que obligaría a migrar cada {a|b|c} existente en la plataforma. {plural N: …} encaja en la superficie que ya existe — mismas llaves, mismas barras, mismo modelo mental de componer por etapas.

Checklist rápido

  • Usa {plural %N%: forma1|forma2|forma3} en cualquier render de número + sustantivo. Incluso en sitios solo en inglés, donde «solo necesitas» 2 formas.
  • Respeta la aridad del locale. RU/UK/BE y SR/HR/BS = 3 formas. Estilo EN = 2. El validador pilla el desajuste.
  • Las formas son texto plano. Sin {} ni [] anidados — extrae antes con #def. No con #set: una macro devuelve los corchetes al instante.
  • Cantidad vacía o no numérica → construcción vacía. Protege con {?HasFoo?…|} si quieres borrar la frase.
  • Los negativos pasan por abs(). El cero toma la forma many. Los decimales fallan la lectura estricta — mantén las cantidades enteras.
  • Fija el locale una vez por plantilla (post meta) o por llamada de render. Aún no hay override por construcción.
  • La ejecución tolerante renderiza las construcciones rotas tal cual, con llaves de ancho completo — fallo visible, no corrupción silenciosa. Los validadores van en estricto.

Pruébalo en vivo

El playgroundEN trae un ejemplo {plural %Count%: language|languages}. Pasa %Count% por 0, 1, 2, 5, 11, 21, 22 para ver la regla de cubos de 2 formas (EN) y luego salta al playground en ruso con el selector de idioma, donde la misma plantilla corre con la regla de 3 formas y las formas rusas del sustantivo.

¿Prefieres un editor de escritorio? Spintax Studio — el editor nativo de Windows de la Microsoft Store — valida sin conexión mientras escribes: su panel de diagnósticos habla los mismos códigos que esta página (plural.arity, plural.count-macro), cada uno con su artículo de ayuda integrado, y un selector de locale cambia la aridad que comprueba el motor — alterna entre en y ru para probar ambas reglas en una sola plantilla.


Continuar la serie