Numerus-Kongruenz: {plural <count>: Form1|Form2|Form3}

Russisch — und jede slawische Sprache — verlangt, dass das Substantiv mit der Grundzahl davor übereinstimmt: 1 язык, 2 языка, 5 языков. Die Wahl hängt von der Anzahl modulo 100 und 10 ab, mit Ausnahmen für 11–14. Spintax ist die erste Engine der Spintax-Familie, die das als Primitiv erster Klasse mitbringt. Vorher hat jede Redaktion die Regel in Templates neu erfunden und irgendwo falsch gemacht — oder Konstruktionen mit Zahlen still vermieden.

Die Syntax

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

Das wörtliche Präfix {plural (mit einem Leerzeichen am Ende) ist das eindeutige Unterscheidungsmerkmal gegenüber der Synonymform {a|b|c}. Der : trennt den Anzahl-Platz vom Formen-Platz. Die Formen werden mit senkrechtem Strich getrennt.

Locale-FamilieSprachenFormen
Ostslawisch ru, uk, be 3: one|few|many
BKS sr, hr, bs 3: one|few|many
EN-Stil (Standard) en, es, pt, de, it, fr, nl, sv, no, da, fi, … 2: one|many

Serbisch, Kroatisch und Bosnisch übernehmen die ostslawische Eimerregel Zeichen für Zeichen — one für 1, 21, 101 (aber nicht 11), few für 2–4, 22–24 (aber nicht 12–14), many für alles übrige, Null eingeschlossen. CLDR nennt diesen dritten Eimer bei BKS other statt many; von der Position her ist es derselbe Platz, ein für russische Stelligkeit geschriebenes Template funktioniert also unverändert.

Arabisch, Walisisch, Hebräisch und Lettisch haben andere Eimerstrukturen und sind bewusst nicht implementiert. Sie kommen sprachweise dazu, sobald echte Nachfrage sichtbar wird.

Beispiele

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

Der Anzahl-Platz nimmt entweder eine %Var%-Referenz oder eine ganze Zahl. Wenn der Plural-Durchgang läuft, hat die Variablenersetzung bereits stattgefunden — der Helfer sieht dort also immer nur einen Integer-String, egal welche Form die Redaktion geschrieben hat.

Die Locale-Regel (RU, 3 Eimer)

Die russische Regel ist berüchtigt kleinteilig. Die vollständige Tabelle:

Anzahl nEimerRU-Beispiel
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 языков

Die Ausnahmen für 11–14 (die nach ihren letzten Ziffern wie one und few aussähen) bringen Behelfslösungen zu Fall. Der Helfer der Engine schließt diese Lücke ein für alle Mal — für jede Redaktion, jeden Zähler, jedes Template. Algorithmus:

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

Negative Zahlen laufen über abs, wie bei CLDR. Die Null nimmt die many-Form („0 языков"), weil das im Russischen die grammatisch richtige Wiedergabe ist — nicht, weil Null ein Sonderfall wäre.

Warum mit Doppelpunkt (und nicht {plural %N%|Formen})

Ein früherer Entwurf schrieb {plural %LangCount%|язык|языка|языков}, nur mit Strichen. Zwei strukturelle Probleme haben diese Form erledigt:

1. Gefahr durch Helfervariablen. Ein übliches Preset-Makro:

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

Würde daraus nach der Variablenersetzung {12|язык|языка|языков}, wäre es von einem vierfachen Synonym nicht zu unterscheiden — und die nächste Pipelinestufe würde fröhlich eines davon zufällig ziehen. Die Doppelpunktform bewahrt das unterscheidende Präfix durch die Expansion hindurch, sodass der Plural-Durchgang gefahrlos nach der Variablenersetzung laufen kann.

2. Ganze Zahlen als Literal. {30|день|дня|дней} kollidiert mit der Synonymform {a|b|c} — der Parser kann sie nicht auseinanderhalten. Die Doppelpunktform macht {plural 30: день|дня|дней} strukturell eindeutig.

Numerische Sonderfälle

Der Anzahl-Platz wird streng geparst. Würde ein Wert stillschweigend etwas anderes bedeuten, als die Redaktion erwartet, löst das Konstrukt stattdessen zu einem leeren String auf.

Anzahl-PlatzErgebnisWarum
12gewählte Formschlichte ganze Zahl
-3gewählte Form für 3abs(), wie CLDR
0gewählte Form (RU: many; EN: many)Null ist grammatisch
12 gewählte Form für 12Leerraum wird entfernt
(leer)ganzes Konstrukt → leerAnzahl fehlt
%MissingVar% (nicht ersetzt)ganzes Konstrukt → leernach der Expansion keine Zahl
1,200ganzes Konstrukt → leerKomma ist keine Ziffer; parseInt würde lügen und 1 liefern
12abc / 08hganzes Konstrukt → leernachfolgende Nicht-Ziffern werden abgelehnt
1.5ganzes Konstrukt → leerin v1 nur ganze Zahlen

Soll bei fehlender Anzahl der ganze Satz verschwinden (statt nur das Konstrukt), schalten Sie den Satz mit einer Bedingung frei:

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

Formen-Platz — keine verschachtelten Spintax-Klammern

Formen müssen reiner Text sein. Sie dürfen keine verschachtelten Spintax-Klammern { } [ ] enthalten. Eine Form wie {plural 1: {a|b}|c} wird abgelehnt, ebenso {plural 1: [<and>day|days]}. Der Validator meldet das als Fehler; die Laufzeit ist nachsichtig und stuft den Block auf Vollbreiten-Klammern herunter, statt zu werfen (siehe unten).

Brauchen Sie wirklich bedingten oder zufälligen Inhalt in einer Form, ziehen Sie ihn vorher in eine Variable — und zwar mit #def, nicht mit #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

Genau dieser Unterschied macht #def wertvoll. #set setzt seinen Wert an jeder Referenz wörtlich ein — ein in ein #set ausgelagertes Synonym bringt die Klammern also genau dorthin zurück, wo Sie sie loswerden wollten. #def würfelt seinen Wert einmal pro Render und übergibt dem Formen-Platz den aufgelösten Text.

Beachten Sie, was da ausgelagert wird: ein nicht flektierendes Fragment, in jeder Form gleich. Nur diese Gestalt trägt das Muster. Lagern Sie kein Substantiv aus, um seine Formen durch Anhängen von Endungen an die Variable zu bauen — %Noun%а passt zu einem Synonym und ergibt beim nächsten Unsinn. Flektiert das Wort selbst, schreiben Sie die Formen aus; dafür sind die Formen-Plätze da.

HTML-Tags (<em>, <a href="…">) und nicht aufgelöste %Var% überleben im Formentext harmlos — verboten sind nur strukturelle Spintax-Klammern.

Nachsichtige Laufzeit — kaputte Konstrukte legen die Seite nicht lahm

Rutscht doch eine Klammer in den Formen-Platz, oder passt die Formenzahl nicht zur Stelligkeit des Locales, fängt die Engine den Fehler blockweise ab und gibt das Konstrukt wörtlich aus, mit Vollbreiten-Klammern (U+FF5B / U+FF5D):

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

Die Vollbreiten-Klammern sehen den ASCII-{} fast gleich, sind aber eigene Codepoints — sie überstehen die nächsten Pipelinestufen, ohne vom Aufzählungsresolver falsch gelesen zu werden. Die Seite rendert, der Fehler ist im HTML sichtbar, und der Betrieb kann ihn ohne 500er beheben.

Der Validator (und der Playground) läuft stattdessen im strengen Modus: Beide Fehlerklassen werfen, mit einem position-Feld und dem wörtlichen Konstrukttext. So fällt der Fehler auf, bevor das Template in Produktion geht.

Woher das Locale kommt

Ein Locale pro Render-Aufruf. Kein Override pro Konstrukt in v1.

  • WordPress-Plugin: Das Post-Meta pro Template _spintax_locale gewinnt; sonst greift das Locale der WordPress-Site (get_locale()).
  • @spintax/core (eigenständig): das Feld locale von render(tpl, { locale }). Woher es kommt, entscheidet der Host — Request-Header, Nutzereinstellung, Site-Konfiguration. Lassen Sie es weg, bekommen Sie den 2-Formen-Standard.
  • Playground: folgt der Seitensprache — Englisch unter /play/EN, Russisch unter /ru/play/. Eine Locale-Umschaltung innerhalb der Seite gibt es nicht; wechseln Sie die Seite über den Sprachumschalter in der Navigation.

Der Locale-String wird auf sein Basis-Tag normalisiert — ru-RUru, uk_UAuk, pt-BRpt. Die Stelligkeitstabelle schlägt nach dem Basis-Tag nach. Schrift- und Regions-Subtags tragen keine Pluralgrammatik, also normalisieren sr-Latn, sr-Cyrl, sr_RS und sr-Latn-RS alle auf sr und bekommen dieselben drei Formen. Dreibuchstabige Tags werden nicht abgebildet: srp bleibt srp und fällt auf den 2-Formen-Standard.

Platz in der 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

Der Plural-Durchgang läuft nach der Variablenexpansion (damit %LangCount% im Anzahl-Platz schon ein Integer-String ist) und vor der Aufzählungsauflösung (damit der Synonymresolver nie die Gelegenheit bekommt, ein fehlerhaftes Konstrukt misszuverstehen).

Durchgespielt: Produktvergleich

Drei Zeilen aus einem SaaS-Vergleichskatalog. Dasselbe Template rendert jede davon, aber die Anzahl steuert sowohl die Substantivform als auch — in der Folge — wie konkret der Text wirkt.

ProduktSprachenIntegrationenTarife
Acme16183
Beta115
Gamma12172

Ohne Plural-Primitiv rendert jedes Produkt dieselbe vage Phrase: „supports many integrations, including Slack, GitHub, Linear". Die Unterschiede im Katalog bleiben unsichtbar. Mit dem Primitiv kann das Template sagen:

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

Gerendert je Zeile:

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

Der sachliche Unterschied steht jetzt im Text. SEO profitiert von echter Differenzierung, Lesende profitieren von konkreten Zahlen statt „viele".

Anti-Patterns

1. Inline-Paarung mit geschlossener Menge

Der Behelf, der „zufällig funktioniert":

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

Jede gewählte Zahl verlangt zufällig die many-Form, das Substantiv passt also nie daneben. Es bricht in dem Moment, in dem die Zahl aus einer echten Variablen kommt — jeder Wert in 21–24 oder 31–34 landet bei der falschen Form.

2. Bedingungen über Eimer-Flags

Der Behelf „Entwickler in der Schleife":

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

Drei Boolesche Flags pro zählbarer Entität im Variablen-Assembler, verschachtelte Bedingungen in jedem Template. Die Redaktion kann keine neue %count% %noun%-Konstruktion schreiben, ohne vorher jemanden zu bitten, das Flag-Trio zu ergänzen und ein Build auszuliefern. Genau diesen Ablauf sollte das Primitiv abschaffen.

3. Listen-Umschreibungen statt Anzahlen

Der Behelf „stilles Ausweichen":

supports many integrations, such as %TopIntegrations%

Die Redaktion schreibt um die Anzahl herum, weil das Werkzeug sie nicht ausdrücken kann. Ergebnis: Jeder Eintrag liest sich gleich, keine SEO-Differenzierung, keine redaktionelle Autorität. Zeigen Sie die Anzahl.

Einordnung in der Branche

Numerus-Kongruenz ist in jedem i18n-Stack ein Primitiv erster Klasse: ICU MessageFormat ({count, plural, one {…} few {…} other {…}}), ngettext aus gettext, FormatJS und so weiter. Sie gehört zu denselben universellen grammatischen Primitiven, ohne die kein ernsthaftes Content-System auskommt.

Was wir anders gemacht haben: Wir haben sie zu einem spintax-eigenen Primitiv gemacht. ICU verlangt eine andere Template-Syntax, was bedeuten würde, jedes vorhandene {a|b|c} der Plattform zu migrieren. {plural N: …} fügt sich in die bestehende Oberfläche — dieselben Klammern, dieselben Striche, dasselbe mentale Modell „in Stufen zusammensetzen".

Kurze Checkliste

  • Nehmen Sie {plural %N%: Form1|Form2|Form3} für jede Ausgabe von Zahl + Substantiv. Auch auf reinen EN-Websites, wo Sie „nur" 2 Formen brauchen.
  • Achten Sie auf die Stelligkeit des Locales. RU/UK/BE und SR/HR/BS = 3 Formen. EN-Stil = 2. Abweichungen fängt der Validator.
  • Formen sind reiner Text. Keine verschachtelten {} oder [] — vorher mit #def auslagern. Nicht mit #set: Ein Makro bringt die Klammern sofort zurück.
  • Leere oder nicht numerische Anzahl → leeres Konstrukt. Mit {?HasFoo?…|} absichern, wenn der ganze Satz verschwinden soll.
  • Negative Zahlen laufen über abs(). Die Null nimmt die many-Form. Dezimalzahlen scheitern an der strengen Prüfung — halten Sie Anzahlen ganzzahlig.
  • Setzen Sie das Locale einmal pro Template (Post-Meta) oder pro Render-Aufruf. Ein Override pro Konstrukt gibt es noch nicht.
  • Die nachsichtige Laufzeit rendert kaputte Konstrukte wörtlich mit Vollbreiten-Klammern — sichtbarer Fehler statt stiller Beschädigung. Validatoren laufen streng.

Live ausprobieren

Der PlaygroundEN enthält ein Beispiel {plural %Count%: language|languages}. Schalten Sie %Count% durch 0, 1, 2, 5, 11, 21, 22, um die 2-Formen-Eimerregel (EN) zu sehen, und wechseln Sie dann über den Sprachumschalter zum russischen Playground, wo dasselbe Template mit der 3-Formen-Regel und russischen Substantivformen läuft.

Lieber ein Desktop-Editor? Spintax Studio — der native Windows-Editor aus dem Microsoft Store — validiert offline beim Tippen: Sein Diagnosepanel spricht dieselben Codes wie diese Seite (plural.arity, plural.count-macro), jeder mit eigenem eingebautem Hilfeartikel, und ein Locale-Auswahlfeld schaltet die Stelligkeit um, die die Engine prüft — wechseln Sie zwischen en und ru, um beide Eimerregeln an einem Template zu testen.


Serie fortsetzen