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 locales | Idiomas | Formas |
|---|---|---|
| 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 n | Balde | Exemplo RU |
|---|---|---|
1, 21, 31, 41, …, 101, 121 | one | 1 язык, 21 язык |
2–4, 22–24, 32–34, … | few | 2 языка, 23 языка |
0, 5–20, 25–30, 35–40, …, 100, 111–114 | many | 0 языков, 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 contagem | Resultado | Por quê |
|---|---|---|
12 | forma escolhida | inteiro simples |
-3 | forma escolhida para 3 | abs(), como no CLDR |
0 | forma escolhida (RU: many; EN: many) | zero é gramatical |
12 | forma escolhida para 12 | espaços são aparados |
| (vazio) | construto inteiro → vazio | contagem ausente |
%MissingVar% (não substituiu) | construto inteiro → vazio | não é número depois da expansão |
1,200 | construto inteiro → vazio | vírgula não é dígito; parseInt mentiria e devolveria 1 |
12abc / 08h | construto inteiro → vazio | caracteres não numéricos no fim são rejeitados |
1.5 | construto inteiro → vazio | só 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_localevence; na falta dele, o locale do site (get_locale()). @spintax/core(avulso): o campolocalederender(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-RU → ru, uk_UA → uk, pt-BR → pt. 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.
| Produto | Idiomas | Integrações | Planos |
|---|---|---|---|
| Acme | 16 | 18 | 3 |
| Beta | 1 | 1 | 5 |
| Gamma | 12 | 17 | 2 |
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ó.