Accord en nombre : {plural <count>: forme1|forme2|forme3}

Le russe — et toute langue slave — exige que le nom s’accorde avec le nombre cardinal qui le précède : 1 язык, 2 языка, 5 языков. Le choix dépend du nombre modulo 100 et 10, avec des exceptions pour 11–14. Spintax est le premier moteur de la famille spintax à livrer cela comme primitive de première classe. Avant, chaque rédaction réinventait la règle dans les templates et se trompait quelque part — ou évitait silencieusement toute tournure comportant un nombre.

La syntaxe

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

Le préfixe littéral {plural (avec une espace finale) est le discriminant sans ambiguïté par rapport à la forme de synonyme {a|b|c}. Les : séparent l’emplacement du nombre de celui des formes. Les formes sont séparées par des barres verticales.

Famille de localesLanguesFormes
Slave oriental ru, uk, be 3 : one|few|many
BCS sr, hr, bs 3 : one|few|many
Style EN (par défaut) en, es, pt, de, it, fr, nl, sv, no, da, fi, … 2 : one|many

Le serbe, le croate et le bosnien reprennent la règle de compartiments du slave oriental caractère pour caractère — one pour 1, 21, 101 (mais pas 11), few pour 2–4, 22–24 (mais pas 12–14), many pour tout le reste, zéro compris. CLDR nomme ce troisième compartiment other plutôt que many pour le BCS ; par la position, c’est le même emplacement, donc un template écrit pour l’arité russe fonctionne sans modification.

L’arabe, le gallois, l’hébreu et le letton ont des structures de compartiments différentes et ne sont volontairement pas implémentés. Ils arriveront langue par langue, à mesure que la demande réelle se manifeste.

Exemples

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

L’emplacement du nombre accepte soit une référence %Var%, soit un entier littéral. Quand la passe de pluriel s’exécute, la substitution des variables a déjà eu lieu — l’assistant n’y voit donc jamais qu’une chaîne entière, quelle que soit la forme écrite par la rédaction.

La règle du locale (RU, 3 compartiments)

La règle russe est réputée pointilleuse. Le tableau complet :

Nombre nCompartimentExemple 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 языков

Les exceptions de 11–14 (qui, à en juger par leurs derniers chiffres, ressembleraient à one et few) font tomber les contournements. L’assistant du moteur comble ce trou une fois pour toutes — pour chaque rédaction, chaque compteur, chaque template. Algorithme :

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

Les nombres négatifs passent par abs, comme dans CLDR. Le zéro prend la forme many (« 0 языков ») parce que c’est la formulation grammaticalement correcte en russe, non parce que le zéro serait un cas particulier.

Pourquoi avec deux-points (et non {plural %N%|formes})

Une première esquisse écrivait {plural %LangCount%|язык|языка|языков}, avec des barres seulement. Deux problèmes structurels ont tué cette forme :

1. Le danger des variables d’aide. Une macro de préréglage courante :

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

Si la construction devenait {12|язык|языка|языков} après substitution, elle serait indiscernable d’un synonyme à quatre branches — et l’étape suivante du pipeline en tirerait joyeusement une au hasard. La forme à deux-points préserve le préfixe discriminant à travers le développement, si bien que la passe de pluriel peut s’exécuter sans risque après la substitution des variables.

2. Les entiers littéraux. {30|день|дня|дней} entre en collision avec la forme de synonyme {a|b|c} — l’analyseur ne peut pas les distinguer. La forme à deux-points rend {plural 30: день|дня|дней} structurellement distincte.

Cas numériques limites

L’emplacement du nombre est analysé strictement. Si une valeur devait signifier en silence autre chose que ce qu’attend la rédaction, la construction se résout en chaîne vide.

Emplacement du nombreRésultatPourquoi
12forme retenueentier simple
-3forme retenue pour 3abs(), comme CLDR
0forme retenue (RU : many ; EN : many)le zéro est grammatical
12 forme retenue pour 12les blancs sont retirés
(vide)construction entière → videnombre absent
%MissingVar% (non substituée)construction entière → videpas un nombre après développement
1,200construction entière → videla virgule n’est pas un chiffre ; parseInt mentirait et renverrait 1
12abc / 08hconstruction entière → videcaractères non numériques en fin rejetés
1.5construction entière → videentiers uniquement en v1

Si vous voulez effacer la phrase entière quand le nombre manque (et pas seulement la construction), protégez la phrase par une condition :

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

Emplacement des formes — pas de crochets spintax imbriqués

Les formes doivent être du texte brut. Elles ne peuvent pas contenir de crochets spintax imbriqués { } [ ]. Une forme comme {plural 1: {a|b}|c} est rejetée, tout comme {plural 1: [<and>day|days]}. Le validateur le signale comme erreur ; l’exécution est tolérante et dégrade le bloc en accolades pleine chasse au lieu de lever une erreur (voir plus bas).

S’il vous faut vraiment du contenu conditionnel ou aléatoire dans une forme, sortez-le d’abord dans une variable — et il faut un #def, pas un #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

C’est cette distinction qui rend #def précieuse. #set substitue sa valeur telle quelle à chaque référence : sortir un synonyme dans un #set remet donc les crochets exactement là où vous vouliez les éviter. #def tire sa valeur une fois par rendu et remet à l’emplacement de forme le texte déjà résolu.

Notez ce qui est extrait : un fragment qui ne varie pas, identique dans toutes les formes. C’est la seule forme que ce motif supporte. N’extrayez pas un nom pour construire ses formes en concaténant des suffixes sur la variable — %Noun%а marche pour un synonyme et produit du charabia au suivant. Quand le mot lui-même varie, écrivez les formes en toutes lettres ; c’est à cela que servent les emplacements de forme.

Les balises HTML (<em>, <a href="…">) et les %Var% non résolues survivent sans dommage dans le texte des formes — seuls les crochets structurels du spintax sont interdits.

Exécution tolérante — une construction cassée n’abat pas la page

Si un crochet se glisse dans l’emplacement de forme, ou si le nombre de formes ne correspond pas à l’arité du locale, le moteur intercepte l’erreur bloc par bloc et émet la construction telle quelle, avec des accolades pleine chasse (U+FF5B / U+FF5D) :

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

Les accolades pleine chasse ressemblent presque aux {} ASCII mais sont des points de code distincts — elles traversent les étapes suivantes du pipeline sans être mal interprétées par le résolveur d’énumérations. La page s’affiche, le bug est visible dans le HTML, et l’exploitation le corrige sans erreur 500.

Le validateur (et le playground) tourne au contraire en mode strict : les deux classes d’erreur lèvent une exception, avec un champ position et le texte littéral de la construction. La rédaction attrape l’erreur avant que le template parte en production.

D’où vient le locale

Un locale par appel de rendu. Pas de surcharge par construction en v1.

  • Extension WordPress : la méta de publication par template _spintax_locale l’emporte ; à défaut, le locale du site WordPress (get_locale()).
  • @spintax/core (autonome) : le champ locale de render(tpl, { locale }). L’hôte décide de la source — en-tête de requête, préférence utilisateur, configuration du site. Omettez-le et vous obtenez le défaut à 2 formes.
  • Playground : suit la langue de la page — anglais sur /play/EN, russe sur /ru/play/. Il n’y a pas de sélecteur de locale dans la page ; changez de page avec le sélecteur de langue de la navigation.

La chaîne de locale est normalisée vers son étiquette de base — ru-RUru, uk_UAuk, pt-BRpt. La table d’arité est consultée par étiquette de base. Les sous-étiquettes d’écriture et de région ne portent aucune grammaire de pluriel : sr-Latn, sr-Cyrl, sr_RS et sr-Latn-RS se normalisent toutes en sr et reçoivent les mêmes trois formes. Les étiquettes à trois lettres ne sont pas mappées : srp reste srp et retombe sur le défaut à 2 formes.

Place dans le 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 passe de pluriel s’exécute après le développement des variables (pour que %LangCount% soit déjà une chaîne entière dans l’emplacement du nombre) et avant la résolution des énumérations (pour que le résolveur de synonymes n’ait jamais l’occasion de mal interpréter une construction mal formée).

Exemple travaillé : comparatif de produits

Trois lignes d’un catalogue comparatif SaaS. Le même template rend chacune, mais le nombre pilote à la fois la forme du nom et — par ricochet — la précision perçue du texte.

ProduitLanguesIntégrationsOffres
Acme16183
Beta115
Gamma12172

Sans primitive de pluriel, chaque produit rend la même phrase vague : « supports many integrations, including Slack, GitHub, Linear ». Les différences du catalogue restent invisibles. Avec la primitive, le template peut dire :

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

Rendu par ligne :

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

La différence factuelle est maintenant dans le texte. Le SEO profite d’une différenciation authentique ; les lecteurs profitent de chiffres concrets au lieu de « beaucoup ».

Anti-patterns

1. Appariement en ligne sur ensemble fermé

Le contournement qui « marche par accident » :

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

Chaque nombre retenu appelle par chance la forme many, le nom ne jure donc jamais. Cela casse dès que le nombre vient d’une vraie variable — toute valeur de 21–24 ou 31–34 tombera sur la mauvaise forme.

2. Conditions à base de drapeaux de compartiment

Le contournement « un développeur dans la boucle » :

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

Trois drapeaux booléens par entité dénombrable dans l’assembleur de variables, des conditions imbriquées dans chaque template. La rédaction ne peut pas écrire une nouvelle tournure %count% %noun% sans demander d’abord à un développeur d’ajouter le trio de drapeaux et de livrer une build. C’est exactement ce flux que la primitive devait supprimer.

3. Enrobages de liste au lieu des nombres

Le contournement de « l’évitement silencieux » :

supports many integrations, such as %TopIntegrations%

La rédaction contourne le nombre parce que l’outil ne sait pas l’exprimer. Résultat : chaque entrée se lit pareil, aucune différenciation SEO, aucune autorité éditoriale. Affichez le nombre.

Contexte du secteur

L’accord en nombre est une primitive de première classe dans toutes les piles i18n : ICU MessageFormat ({count, plural, one {…} few {…} other {…}}), ngettext de gettext, FormatJS, etc. Elle appartient à cette catégorie de primitives grammaticales universelles sans lesquelles aucun système de contenu sérieux ne se conçoit.

Ce que nous avons fait différemment : nous en avons fait une primitive native du spintax. ICU impose une autre syntaxe de template, ce qui obligerait à migrer chaque {a|b|c} existant de la plateforme. {plural N: …} s’insère dans la surface déjà en place — mêmes accolades, mêmes barres, même modèle mental de composition par étapes.

Checklist rapide

  • Utilisez {plural %N%: forme1|forme2|forme3} pour tout rendu de nombre + nom. Même sur des sites uniquement en anglais, où vous « n’avez besoin » que de 2 formes.
  • Respectez l’arité du locale. RU/UK/BE et SR/HR/BS = 3 formes. Style EN = 2. Le validateur attrape les écarts.
  • Les formes sont du texte brut. Pas de {} ni de [] imbriqués — extrayez d’abord via #def. Pas #set : une macro remet les crochets aussitôt.
  • Nombre vide ou non numérique → construction vide. Protégez par {?HasFoo?…|} si vous voulez effacer la phrase.
  • Les nombres négatifs passent par abs(). Le zéro prend la forme many. Les décimaux échouent au test strict — gardez des nombres entiers.
  • Fixez le locale une fois par template (méta de publication) ou par appel de rendu. Pas encore de surcharge par construction.
  • L’exécution tolérante rend les constructions cassées telles quelles, en accolades pleine chasse — bug visible plutôt que corruption silencieuse. Les validateurs, eux, sont stricts.

Essayez en direct

Le playgroundEN contient un exemple {plural %Count%: language|languages}. Faites passer %Count% par 0, 1, 2, 5, 11, 21, 22 pour voir la règle à 2 formes (EN), puis basculez vers le playground russe via le sélecteur de langue, où le même template tourne avec la règle à 3 formes et les formes russes du nom.

Vous préférez un éditeur de bureau ? Spintax Studio — l’éditeur Windows natif du Microsoft Store — valide hors ligne pendant que vous tapez : son panneau de diagnostics parle les mêmes codes que cette page (plural.arity, plural.count-macro), chacun avec son article d’aide intégré, et un sélecteur de locale change l’arité vérifiée par le moteur — passez de en à ru pour tester les deux règles sur un même template.


Continuer la série