Concordanza di numero: {plural <count>: forma1|forma2|forma3}

In italiano il plurale è semplice: 1 lingua, 3 lingue, due forme e via. Il russo — e ogni lingua slava — chiede invece che il sostantivo concordi con il numerale che lo precede: 1 язык, 2 языка, 5 языков. La scelta dipende dal numero modulo 100 e modulo 10, con eccezioni per 11–14. Spintax è il primo motore della famiglia spintax a offrirlo come primitiva di prima classe. Prima ogni redazione la reinventava nei template e sbagliava da qualche parte — oppure evitava in silenzio qualsiasi costruzione con un numero dentro.

La sintassi

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

Il prefisso letterale {plural (con uno spazio finale) è il discriminante inequivocabile rispetto alla forma sinonimica {a|b|c}. I : separano lo slot del numero da quello delle forme. Le forme sono separate dalla barra verticale.

Famiglia di localeLingueForme
Slavo orientale ru, uk, be 3: one|few|many
BCS sr, hr, bs 3: one|few|many
Stile EN (predefinito) en, es, pt, de, it, fr, nl, sv, no, da, fi, … 2: one|many

Serbo, croato e bosniaco riusano la regola dei bucket slavo-orientale carattere per carattere — one per 1, 21, 101 (ma non 11), few per 2–4, 22–24 (ma non 12–14), many per tutto il resto, zero compreso. CLDR chiama other quel terzo bucket per il BCS invece di many; posizionalmente è lo stesso slot, quindi un template scritto per l’arità russa funziona senza modifiche.

Arabo, gallese, ebraico e lettone hanno strutture di bucket diverse e di proposito non sono implementati. Arriveranno lingua per lingua quando emergerà una domanda reale.

Esempi

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

Lo slot del numero accetta un riferimento %Var% oppure un intero letterale. Quando la passata dei plurali gira, la sostituzione delle variabili è già avvenuta — quindi l’helper vede sempre e solo una stringa numerica intera nello slot, quale che sia la forma scritta dalla redazione.

La regola di locale (i 3 bucket del russo)

La regola russa è notoriamente capziosa. La tabella completa:

Numero nBucketEsempio 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 языков

Le eccezioni per 11–14 (che dalle ultime cifre sembrerebbero one e few) fanno cadere qualunque ripiego. L’helper del motore chiude quel buco una volta per ogni redazione, ogni contatore e ogni 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

I numeri negativi passano per abs, come in CLDR. Lo zero prende la forma many («0 языков») perché in russo si scrive così, non perché lo zero sia un caso speciale.

Perché con i due punti (e non {plural %N%|forme})

Una bozza precedente scriveva {plural %LangCount%|язык|языка|языков}, solo con le barre. Due problemi strutturali hanno affossato quella forma:

1. Il pericolo della variabile d’appoggio. Una macro di preset molto comune:

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

Se il costrutto diventasse {12|язык|языка|языков} dopo la sostituzione delle variabili, sarebbe indistinguibile da un sinonimo a quattro rami — e la fase successiva della pipeline ne sceglierebbe volentieri uno a caso. La forma con i due punti conserva il prefisso discriminante attraverso l’espansione, così la passata dei plurali può girare in sicurezza dopo la sostituzione delle variabili.

2. Gli interi letterali. {30|день|дня|дней} collide con la forma sinonimica {a|b|c}: il parser non può distinguerle. La forma con i due punti rende {plural 30: день|дня|дней} strutturalmente diverso.

Casi numerici limite

Lo slot del numero è analizzato in modo severo. Se uno slot rischiasse di significare in silenzio qualcosa di diverso da quello che la redazione si aspetta, il costrutto si risolve invece nella stringa vuota.

Slot del numeroRisultatoPerché
12forma sceltaintero semplice
-3forma scelta per 3abs(), come in CLDR
0forma scelta (RU: many; EN: many)lo zero è grammaticale
12 forma scelta per 12gli spazi vengono rifilati
(vuoto)l’intero costrutto → vuotonumero mancante
%MissingVar% (non sostituita)l’intero costrutto → vuotodopo l’espansione non è un numero
1,200l’intero costrutto → vuotola virgola non è una cifra; parseInt mentirebbe restituendo 1
12abc / 08hl’intero costrutto → vuotocaratteri non numerici in coda rifiutati
1.5l’intero costrutto → vuotonella v1 solo interi

Se volete cancellare l’intera frase quando manca il numero (e non solo il costrutto), chiudetela dentro una condizione:

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

Slot delle forme — niente parentesi spintax annidate

Le forme devono essere testo semplice. Non possono contenere parentesi spintax annidate { } [ ]. Una forma come {plural 1: {a|b}|c} viene rifiutata, e così {plural 1: [<and>day|days]}. Il validatore la segnala come errore; il runtime è tollerante e degrada il blocco a parentesi a tutta larghezza invece di sollevare un’eccezione (vedi sotto).

Se vi serve davvero contenuto condizionale o casuale dentro una forma, portatelo prima in una variabile — e dev’essere #def, non #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

È questa la distinzione che rende #def degno di esistere. #set sostituisce il proprio valore alla lettera a ogni riferimento, quindi portare un sinonimo dentro un #set rimette le parentesi proprio nello slot che volevate tenere pulito. #def tira il proprio valore una volta per render e consegna allo slot il testo già risolto.

Notate che cos’è il valore portato fuori: un frammento che non si flette, identico in ogni forma. È l’unica configurazione che questo schema regge. Non portate fuori un sostantivo per poi costruirne le forme concatenando desinenze alla variabile — %Noun%а funziona per un sinonimo e produce spazzatura per il successivo. Quando la parola stessa si flette, scrivete le forme per esteso: gli slot delle forme servono a questo.

I tag HTML (<em>, <a href="…">) e le %Var% non risolte sopravvivono senza danni nel testo delle forme — sono vietate solo le parentesi spintax strutturali.

Runtime tollerante — un costrutto rotto non fa cadere la pagina

Se una parentesi si intrufola in uno slot di forma, o se il numero di forme non corrisponde all’arità del locale, il motore intercetta l’errore blocco per blocco ed emette il costrutto alla lettera con le parentesi a tutta larghezza (U+FF5B / U+FF5D):

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

Le parentesi a tutta larghezza sembrano quasi identiche alle {} ASCII ma sono codepoint diversi: attraversano le fasi successive della pipeline senza che il risolutore delle enumerazioni le interpreti male. La pagina viene resa, il difetto è visibile nell’HTML e chi gestisce il sito può correggerlo senza un 500.

Il validatore (e il playground) gira invece in modalità severa: entrambe le classi di errore sollevano un’eccezione con un campo position e il testo letterale del costrutto. Chi scrive intercetta l’errore prima che il template arrivi in produzione.

Da dove viene il locale

Un locale per chiamata di render. Nella v1 non c’è override per singolo costrutto.

  • Plugin WordPress: vince il post meta per template _spintax_locale; in mancanza si ricade sul locale del sito WordPress (get_locale()).
  • @spintax/core (standalone): il campo locale di render(tpl, { locale }). La fonte la decide l’host: header della richiesta, impostazione utente, configurazione del sito. Omettetelo e ottenete il default a 2 forme.
  • Playground: segue la lingua della pagina — inglese su /play/, russo su /ru/play/. Non c’è un controllo di locale per pagina; si cambia pagina con il selettore di lingua nella navigazione.

La stringa di locale è normalizzata al tag base — ru-RUru, uk_UAuk, pt-BRpt. La tabella delle arità si consulta per tag base. I sottotag di scrittura e di regione non portano grammatica del plurale, quindi sr-Latn, sr-Cyrl, sr_RS e sr-Latn-RS si normalizzano tutti in sr e ottengono le stesse tre forme. I tag a tre lettere non sono mappati: srp resta srp e ricade sul default a 2 forme.

Posto nella 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

La passata dei plurali gira dopo l’espansione delle variabili (così %LangCount% nello slot del numero è già una stringa intera letterale) e prima della risoluzione delle enumerazioni (così il risolutore dei sinonimi non ha mai occasione di fraintendere un costrutto malformato).

Esempio pratico: confronto di prodotti

Tre righe da un catalogo comparativo di prodotti SaaS. Le rende lo stesso identico template, ma il numero guida sia la forma del sostantivo sia — di conseguenza — la specificità percepita del testo.

ProdottoLingueIntegrazioniPiani
Acme16183
Beta115
Gamma12172

Senza una primitiva per i plurali, ogni prodotto rende la stessa frase vaga: «supports many integrations, including Slack, GitHub, Linear». Le differenze del catalogo restano invisibili. Con la primitiva, il template può dire:

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

Ecco le rese riga per riga:

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

La differenza di fatto ora è nel testo. La SEO guadagna da una differenziazione autentica; chi legge guadagna numeri concreti al posto di «molte».

Anti-pattern

1. Accoppiamento inline su insieme chiuso

Il ripiego che «funziona per caso»:

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

Ogni numero previsto prende per caso la forma many, così il sostantivo non discorda mai. Si rompe nell’istante in cui il numero arriva da una variabile reale: qualsiasi valore fra 21–24 o 31–34 si accoppierà con la forma sbagliata.

2. Condizioni su flag di bucket

Il ripiego «con l’ingegnere nel giro»:

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

Tre flag booleani in più per ogni entità numerabile nell’assemblatore di variabili, e condizioni annidate in ogni template. Chi scrive non può comporre una nuova costruzione %count% %noun% senza prima chiedere a uno sviluppatore di aggiungere la terna di flag, pubblicare una build e solo allora scrivere il template. È esattamente il flusso di lavoro che la primitiva è nata per eliminare.

3. Involucri a lista al posto dei numeri

Il ripiego dell’«evitamento silenzioso»:

supports many integrations, such as %TopIntegrations%

Chi scrive gira intorno al numero perché lo strumento non sa esprimerlo. Il risultato: ogni voce si legge identica, nessuna differenziazione per la SEO, nessuna autorevolezza editoriale. Fate emergere il numero.

Contesto di settore

La concordanza di numero è una primitiva di prima classe in ogni stack i18n: ICU MessageFormat ({count, plural, one {…} few {…} other {…}}), ngettext di gettext, FormatJS e così via. Appartiene alla stessa categoria di primitive grammaticali universali senza cui nessun sistema di contenuti serio può stare in piedi.

Che cosa abbiamo fatto di diverso: l’abbiamo resa una primitiva nativa di spintax. ICU richiede una sintassi di template diversa, il che vorrebbe dire migrare ogni {a|b|c} già presente sulla piattaforma. {plural N: …} si innesta sulla superficie esistente — stesse graffe, stesse barre, stesso modello mentale del «comporre per fasi».

Checklist rapida

  • Usate {plural %N%: forma1|forma2|forma3} per qualsiasi resa di numero + sostantivo. Anche su un sito solo inglese dove «vi servono» appena 2 forme.
  • Rispettate l’arità del locale. RU/UK/BE e SR/HR/BS = 3 forme. Stile EN = 2. Il disallineamento lo prende il validatore.
  • Le forme sono testo semplice. Niente {} o [] annidati — portateli prima fuori con #def. Non con #set: è una macro e le parentesi tornano dentro.
  • Numero vuoto o non numerico → costrutto vuoto. Chiudete in {?HasFoo?…|} se volete cancellare la frase intera.
  • I negativi passano per abs(). Lo zero prende la forma many. I decimali non passano il controllo severo: tenete i numeri interi.
  • Impostate il locale una volta per template (post meta) o per chiamata di render. Non c’è ancora un override per costrutto.
  • Il runtime tollerante rende i costrutti rotti alla lettera con parentesi a tutta larghezza: difetto visibile, non corruzione silenziosa. I validatori girano in modalità severa.

Provatelo dal vivo

Il playgroundEN contiene un esempio {plural %Count%: language|languages}. Portate %Count% su 0, 1, 2, 5, 11, 21, 22 per vedere la regola dei bucket a 2 forme (EN), poi passate al playground russo dal selettore di lingua nella navigazione per vedere lo stesso template con la regola a 3 forme e le forme russe del sostantivo.

Preferite un editor da scrivania? Spintax Studio — l’editor nativo per Windows del Microsoft Store — valida mentre scrivete, offline: il pannello di diagnostica parla gli stessi codici di questa pagina (plural.arity, plural.count-macro), ognuno con il suo articolo di aiuto integrato, e un selettore di locale cambia l’arità che il motore controlla — alternate en e ru per provare entrambe le regole su un solo template.


Continua la serie