Skip to main content

Plurals, ordinals and genders

A ternary written in your language cannot produce the forms another language needs. French has two plural forms; Russian and Polish have four, Arabic six. count === 1 ? "1 article" : "{count} articles" gives two branches, so Russian gets a grammatically wrong sentence, and nothing tells you.

So you do not write the branches. You write one form, with {count} where the number goes, and pass the number as count. The model writes the forms each language needs — the primary language included — and the SDK picks the right one for the number.

Plurals: count

import { I18nKeylessText } from "i18n-keyless-react";

function CartLine({ items }: { items: number }) {
return (
<I18nKeylessText count={items}>
{"{count} articles dans votre panier"}
</I18nKeylessText>
);
}

What the reader sees:

countFrench (primary)Russian
11 article dans votre panier1 товар в корзине
33 articles dans votre panier3 товара в корзине
55 articles dans votre panier5 товаров в корзине
2121 articles dans votre panier21 товар в корзине

You never wrote the Russian few and many branches, and you never wrote "1 article" either: the primary language gets its own forms from the model too, so a key rendered with count is looked up in every language, the primary one included.

How it works

The API stores, for each language, one ICU MessageFormat message instead of a plain sentence:

{count, plural, one {{count} товар в корзине} few {{count} товара в корзине} many {{count} товаров в корзине} other {{count} товара в корзине}}

It asks the model for exactly the CLDR plural categories of that language and checks the answer against Intl.PluralRules before storing it: a Russian message without a few branch is asked again, not saved. On your side the SDK picks the branch with the same Intl.PluralRules — the tables every browser and Node ship — and fills {count}. Nothing to install, no plural table in your bundle.

{count} is filled like a replace placeholder. Your own replace map wins if it names {count}, so a formatted number goes through unchanged:

<I18nKeylessText count={total} replace={{ "{count}": total.toLocaleString(lang) }}>
{"{count} articles"}
</I18nKeylessText>

The row's key stays the source text, {count} articles dans votre panier. In the dashboard the cell shows the whole message, one branch per form, and you can edit any branch by hand like any other translation.

Ordinals: count + ordinal

A rank (1st, 2nd, 3rd — 1er, 2e) follows other rules than a quantity. English has four ordinal forms, French two, Russian one. Add ordinal:

<I18nKeylessText count={rank} ordinal>{"Vous êtes {count}e"}</I18nKeylessText>
countEnglishFrench
1You are 1stVous êtes 1er
2You are 2ndVous êtes 2e
3You are 3rdVous êtes 3e
11You are 11thVous êtes 11e
22You are 22ndVous êtes 22e

Genders and other closed choices: select

A gender, a role, a day of the week: a closed set of values that changes the wording. Pass the current value in select. The model writes one variant of the sentence per value, in every language, with the agreement that language needs:

<I18nKeylessText select={{ gender: user.gender }}>Il est connecté</I18nKeylessText>
genderFrenchPolish
maleIl est connectéJest zalogowany
femaleElle est connectéeJest zalogowana
anything elseConnectéTa osoba jest zalogowana

The row is written with the values it has seen. The first render with gender: "female" creates the female branch; a later render with a value the row never saw sends it to the API, which rewrites every language with the new branch, and the row renders its neutral other branch until then. Values are identifiers (male, she_her, admin), at most 10 per variable and 2 variables per sentence. A select and a count can be combined: the plural is nested inside each variant.

What stays a replace: names and other open values

A user's name is not a closed set. No library declines an unknown proper noun in Russian, i18next included: the sentence is translated, then the name is pasted in. That does not change here — but the model is asked to place such a placeholder where the language needs no inflection, and to rephrase the sentence around it when it does. Bienvenue {name} is Добро пожаловать, {name} (nominative); Un message de {name} becomes Сообщение от пользователя {name}, where the declension lands on "пользователя", never on the name.

When the value really has to be inflected, the whole sentence with the value in it is the key — the user generated content flow — at one translation per distinct value. Use it for rare cases only.

Older SDKs and the other ports

A row upgraded to plural forms by one app keeps its forms for every other client of the project. An SDK that does not send count receives the message and renders its other branch — the form you wrote — with {count} filled by replace as before. The Laravel, Rails, Flutter, Python, Go, Swift and Kotlin ports do not render the forms yet: they return the message as stored.