What is Pluralization?

Pluralization is choosing the right form of a message based on a count. English has two forms, "1 file" and "2 files", so developers are tempted to hack it with an if statement. Other languages break that hack immediately: some use one form for everything, while others, Russian and Arabic among them, have several forms selected by rules more complex than "equal to one".

The CLDR project defines standard plural categories per language: zero, one, two, few, many, and other. A proper i18n layer lets the translation itself declare which forms it needs, and picks the right one at runtime from the count and the active locale.

Laravel handles this with trans_choice() and a pipe syntax in the translation string, including exact-count and range segments. For more complex grammar, ICU MessageFormat plurals via the intl extension are the heavier-duty option. Note that ordinal numbers (1st, 2nd, 3rd) follow separate rules from cardinal counts.

// lang/en/messages.php
return [
    'apples' => '{0} No apples|{1} One apple|[2,*] :count apples',
];

trans_choice('messages.apples', 0);              // "No apples"
trans_choice('messages.apples', 7, ['count' => 7]); // "7 apples"

How trans_choice() picks a form

trans_choice() works in two passes. First it checks segments with explicit conditions: {0} matches exactly zero, [2,10] matches a range and [11,*] anything from eleven up. If one matches, it wins. Otherwise Laravel strips the conditions and picks a segment by position, using a plural index computed from the count and the locale.

The index comes from a switch statement in Laravel's MessageSelector that covers a long list of locale codes: English and German get two forms, Russian and Polish three, Arabic six. If the string has fewer segments than the index asks for, Laravel quietly returns the first one. trans_choice() also fills in :count for you, and accepts an array or a Countable, which it counts first.

// lang/ru/files.php
return [
    'count' => ':count файл|:count файла|:count файлов',
];

// With the app locale set to ru
trans_choice('files.count', 1);  // "1 файл"
trans_choice('files.count', 3);  // "3 файла"
trans_choice('files.count', 5);  // "5 файлов"
trans_choice('files.count', 21); // "21 файл"
trans_choice('files.count', 22); // "22 файла"

Common mistakes

The most frequent one is copying the English two-segment structure into every language. A Russian string with only two segments looks right for 1 and 3, then prints 5 файл, which is wrong: the index for 5 is 2, that segment does not exist, and Laravel falls back to the first one without any warning.

A locale code Laravel does not recognize breaks every plural string at once. The switch matches codes like pt_BR and en_US; pt-BR with a hyphen, or a regional code that is not in the list such as es_419, falls through to the default branch and always gets the first form. Normalize locale codes to underscores and test plural output for each regional locale you ship.

The rest are smaller but common: writing if ($count === 1) in code, which hardcodes English grammar; forgetting zero, which usually deserves its own wording (No files rather than 0 files); and concatenating the number outside the translated string, which stops languages that put the number elsewhere from moving it.

use Illuminate\Translation\MessageSelector;

$selector = new MessageSelector;

$selector->getPluralIndex('pt_BR', 3);  // 1
$selector->getPluralIndex('pt-BR', 3);  // 0, the first form for every count
$selector->getPluralIndex('es_419', 3); // 0

Ordinals follow different rules

Cardinal plurals answer how many; ordinals answer which position, and the two follow different rules in the same language. English cardinals have two forms, but English ordinals have four suffixes (1st, 2nd, 3rd, 4th), with 11th, 12th and 13th as exceptions. trans_choice() handles cardinals only.

For ordinals, Laravel's Number::ordinal() and Number::spellOrdinal() format through the intl extension and follow the locale you pass. When the wording around the number changes, ICU MessageFormat's selectordinal lets the translation choose a branch by ordinal category.

use Illuminate\Support\Number;

Number::ordinal(22, 'en');     // "22nd"
Number::ordinal(22, 'fr');     // "22e"
Number::spellOrdinal(3, 'en'); // "third"

How to test plural strings

Test with counts that reach every category, not just 1 and 2. A practical set is 0, 1, 2, 5, 11, 21, 22 and 101: together they cover the zero wording, the singular, the few and many categories of Slavic languages and the teens exceptions. Assert on the rendered string for every locale you ship, and add a check that each translation has as many segments as its language needs.

Put the theory to work

LangSyncer manages your Laravel translations live: import lang files, translate with AI and publish without a deploy.

We'd like to use analytics and marketing cookies to understand how the site is used. They stay off unless you accept them. Cookie Policy