Concordância de número: {plural <count>: forma1|forma2|forma3}

O russo — e toda língua eslava — exige que o substantivo concorde com o numeral cardinal na frente dele: 1 язык, 2 языка, 5 языков. A escolha depende da contagem módulo 100 e 10, com exceções para 11–14. O Spintax é o primeiro motor da família spintax a trazer isso como primitivo de primeira classe. Antes, cada editor reinventava a regra nos templates e errava em algum ponto — ou evitava em silêncio qualquer construção com número.

A sintaxe

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

O prefixo literal {plural (com um espaço no fim) é o discriminador inequívoco em relação à forma de sinônimo {a|b|c}. Os : separam o espaço da contagem do espaço das formas. As formas são separadas por barra vertical.

Família de localesIdiomasFormas
Eslavo oriental ru, uk, be 3: one|few|many
BCS sr, hr, bs 3: one|few|many
Estilo EN (padrão) en, es, pt, de, it, fr, nl, sv, no, da, fi, … 2: one|many

Sérvio, croata e bósnio reaproveitam a regra de baldes do eslavo oriental caractere por caractere — one para 1, 21, 101 (mas não 11), few para 2–4, 22–24 (mas não 12–14), many para todo o resto, incluindo zero. O CLDR chama esse terceiro balde de other em vez de many no BCS; posicionalmente é o mesmo espaço, então um template escrito com a aridade do russo funciona sem mudanças.

Árabe, galês, hebraico e letão têm estruturas de balde diferentes e não estão implementados de propósito. Vão entrar idioma a idioma, conforme a demanda real aparecer.

Exemplos

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: день|дня|дней}

O espaço da contagem aceita uma referência %Var% ou um inteiro literal. Quando a passagem de plural roda, a substituição de variáveis já aconteceu — então o auxiliar só vê uma string de inteiro no espaço da contagem, não importa qual das duas formas o editor escreveu.

A regra do locale (3 baldes do RU)

A regra russa é famosa por ser chata. A tabela completa:

Contagem nBaldeExemplo 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 языков

As exceções de 11–14 (que, pelos últimos dígitos, pareceriam one e few) derrubam as gambiarras. O auxiliar do motor fecha esse buraco de uma vez, para todo editor, todo contador e todo template. 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

Números negativos usam abs, como no CLDR. O zero pega a forma many ("0 языков") porque é assim que se escreve corretamente em russo, não porque o zero seja um caso especial.

Por que com dois-pontos (e não {plural %N%|formas})

Um esboço anterior escrevia {plural %LangCount%|язык|языка|языков}, só com barras. Dois problemas estruturais mataram essa forma:

1. Perigo da variável auxiliar. Uma macro comum de preset:

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

Se o construto virasse {12|язык|языка|языков} depois da substituição de variáveis, ficaria indistinguível de um sinônimo de quatro opções — e a etapa seguinte do pipeline escolheria uma ao acaso, feliz da vida. A forma com dois-pontos preserva o prefixo discriminador através da expansão, então a passagem de plural pode rodar com segurança depois da substituição de variáveis.

2. Inteiros literais. {30|день|дня|дней} colide com o formato de sinônimo {a|b|c} — o parser não consegue distinguir. A forma com dois-pontos torna {plural 30: день|дня|дней} estruturalmente distinta.

Casos numéricos de borda

O espaço da contagem é lido com rigor. Se um valor fosse significar em silêncio algo diferente do que o editor espera, o construto resolve para string vazia.

Espaço da contagemResultadoPor quê
12forma escolhidainteiro simples
-3forma escolhida para 3abs(), como no CLDR
0forma escolhida (RU: many; EN: many)zero é gramatical
12 forma escolhida para 12espaços são aparados
(vazio)construto inteiro → vaziocontagem ausente
%MissingVar% (não substituiu)construto inteiro → vazionão é número depois da expansão
1,200construto inteiro → vaziovírgula não é dígito; parseInt mentiria e devolveria 1
12abc / 08hconstruto inteiro → vaziocaracteres não numéricos no fim são rejeitados
1.5construto inteiro → vaziosó inteiros na v1

Se você quer apagar a frase inteira quando a contagem falta (e não só o construto), proteja a frase com um condicional:

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

Espaço das formas — sem colchetes de spintax aninhados

As formas precisam ser texto puro. Elas não podem conter colchetes de spintax aninhados { } [ ]. Uma forma como {plural 1: {a|b}|c} é rejeitada, e {plural 1: [<and>day|days]} também. O validador reporta como erro; o runtime é tolerante e degrada o bloco para chaves de largura total em vez de estourar (veja abaixo).

Se você realmente precisa de conteúdo condicional ou aleatório dentro de uma forma, tire-o antes para uma variável — e ela precisa ser #def, não #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

É essa a distinção que faz o #def valer a pena. O #set substitui o valor tal e qual em cada referência, então tirar um sinônimo para um #set devolve os colchetes exatamente ao espaço que você queria manter limpo. O #def sorteia o valor uma vez por render e entrega ao espaço da forma o texto já resolvido.

Repare no que foi extraído: um fragmento que não flexiona, igual em todas as formas. É o único formato que esse padrão aceita. Não tire o substantivo para uma variável tentando montar as formas concatenando sufixos — %Noun%а funciona para um sinônimo e vira lixo no seguinte. Quando a própria palavra flexiona, escreva as formas por extenso; é para isso que os espaços de forma existem.

Tags HTML (<em>, <a href="…">) e %Var% não resolvidas sobrevivem sem estrago no texto da forma — só os colchetes estruturais do spintax são proibidos.

Runtime tolerante — construto quebrado não derruba a página

Se um colchete escapar para o espaço da forma, ou a quantidade de formas não bater com a aridade do locale, o motor captura o erro por bloco e emite o construto tal e qual, com chaves de largura total (U+FF5B / U+FF5D):

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

As chaves de largura total parecem quase idênticas às {} ASCII, mas são codepoints distintos — atravessam as etapas seguintes do pipeline sem serem mal interpretadas pelo resolvedor de enumerações. A página renderiza, o bug fica visível no HTML e a operação conserta sem um 500.

O validador (e o playground) roda em modo estrito: as duas classes de erro estouram com um campo position e o texto literal do construto. Quem escreve pega o erro antes de o template chegar à produção.

De onde vem o locale

Um locale por chamada de render. Sem override por construto na v1.

  • Plugin do WordPress: o post meta por template _spintax_locale vence; na falta dele, o locale do site (get_locale()).
  • @spintax/core (avulso): o campo locale de render(tpl, { locale }). Quem hospeda decide a fonte — cabeçalho da requisição, preferência do usuário, configuração do site. Omita e você fica com o padrão de 2 formas.
  • Playground: segue o idioma da página — inglês em /play/EN, russo em /ru/play/. Não há controle de locale dentro da página; troque de página pelo seletor de idioma da barra de navegação.

A string do locale é normalizada para a tag base — ru-RUru, uk_UAuk, pt-BRpt. A tabela de aridade consulta pela tag base. Subtags de script e região não carregam gramática de plural, então sr-Latn, sr-Cyrl, sr_RS e sr-Latn-RS normalizam todas para sr e recebem as mesmas três formas. Tags de três letras não são mapeadas: srp continua srp e cai no padrão de 2 formas.

Lugar no pipeline

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

A passagem de plural roda depois da expansão de variáveis (para que %LangCount% no espaço da contagem já seja uma string de inteiro) e antes da resolução de enumerações (para que o resolvedor de sinônimos nunca tenha chance de interpretar mal um construto malformado).

Exemplo prático: comparação de produtos

Três linhas de um catálogo comparativo de SaaS. O mesmo template renderiza cada uma, mas a contagem comanda tanto a forma do substantivo quanto — por consequência — o grau de especificidade que o texto transmite.

ProdutoIdiomasIntegraçõesPlanos
Acme16183
Beta115
Gamma12172

Sem um primitivo de plural, todo produto renderiza a mesma frase vaga: "supports many integrations, including Slack, GitHub, Linear". As diferenças do catálogo ficam invisíveis. Com o primitivo, o template consegue dizer:

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

Renderiza, por linha:

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

A diferença factual agora está no texto. O SEO ganha com diferenciação autêntica; quem lê ganha números concretos no lugar de "muitas".

Antipadrões

1. Emparelhamento inline de conjunto fechado

A gambiarra que "funciona por acidente":

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

Todo número escolhido calha de pedir a forma many, então o substantivo nunca discorda. Quebra no instante em que o número vem de uma variável real — qualquer valor em 21–24 ou 31–34 vai se emparelhar com a forma errada.

2. Condicionais por flag de balde

A gambiarra do "engenheiro no meio do caminho":

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

Adicione três flags booleanas por entidade contável no montador de variáveis e escreva condicionais aninhados em todo template. Quem escreve não consegue criar uma construção %count% %noun% nova sem antes pedir a um engenheiro que acrescente o trio de flags, publique um build e só então escrever o template. É exatamente esse fluxo que o primitivo veio eliminar.

3. Embrulhar listas em vez de contar

A gambiarra do "desvio silencioso":

supports many integrations, such as %TopIntegrations%

Quem escreve contorna a contagem porque a ferramenta não sabe expressá-la. Resultado: toda entrada lê igual, sem diferenciação de SEO e sem autoridade editorial. Mostre a contagem.

Contexto do mercado

A concordância de número é primitivo de primeira classe em toda pilha de i18n: ICU MessageFormat ({count, plural, one {…} few {…} other {…}}), ngettext do gettext, FormatJS e por aí vai. Pertence à mesma categoria de primitivos gramaticais universais sem os quais nenhum sistema sério de conteúdo se sustenta.

O que fizemos de diferente: tornamos isso um primitivo nativo do spintax. O ICU exige outra sintaxe de template, o que significaria migrar cada {a|b|c} existente na plataforma. O {plural N: …} encaixa na superfície que já existe — mesmas chaves, mesmas barras, mesmo modelo mental de "compor por etapas".

Checklist rápido

  • Use {plural %N%: forma1|forma2|forma3} em qualquer renderização de número + substantivo. Até em sites só em inglês, onde você "só precisa" de 2 formas.
  • Respeite a aridade do locale. RU/UK/BE e SR/HR/BS = 3 formas. Estilo EN = 2. A divergência é pega pelo validador.
  • As formas são texto puro. Sem {} nem [] aninhados — extraia antes com #def. Não com #set: uma macro devolve os colchetes na hora.
  • Contagem vazia ou não numérica → construto vazio. Proteja com {?HasFoo?…|} se quiser apagar a frase toda.
  • Números negativos usam abs(). O zero pega a forma many. Decimais falham na leitura estrita — mantenha as contagens inteiras.
  • Defina o locale uma vez por template (post meta) ou por chamada de render. Ainda não há override por construto.
  • O runtime tolerante renderiza construtos quebrados tal e qual, com chaves de largura total — bug visível, não corrupção silenciosa. Validadores rodam em modo estrito.

Teste ao vivo

O playgroundEN já vem com um exemplo {plural %Count%: language|languages}. Passe %Count% por 0, 1, 2, 5, 11, 21, 22 para ver a regra de baldes de 2 formas (EN) e depois vá para o playground em russo pelo seletor de idioma da navegação, para ver o mesmo template com a regra de 3 formas e as formas russas do substantivo.

Prefere um editor de desktop? O Spintax Studio — o editor nativo de Windows da Microsoft Store — valida enquanto você digita, offline: o painel de diagnóstico fala os mesmos códigos desta página (plural.arity, plural.count-macro), cada um com seu artigo de ajuda embutido, e um seletor de locale troca a aridade que o motor verifica — alterne entre en e ru para testar as duas regras de balde num template só.


Continuar a série