Термин пришёл из интернационализации (i18n): plural – множественное число, плюрализация – выбор корректной формы слова под конкретное число. В интерфейсах задача встречается на каждом шагу: счётчик корзины («5 товаров»), комментарии («3 комментария»), таймеры обратного отсчёта на setInterval («осталось 2 дня»), интервалы времени, посчитанные через Date.now() («4 часа назад»). В английском форм всего две (product/products), поэтому наивные библиотеки просто прибавляют «s». В русском для целых чисел форм три, и выбор зависит не от величины числа, а от его последних цифр: 1 товар, 2 товара, 5 товаров, затем снова 21 товар – но 11 товаров.
Как это работает
Правила множественного числа для языков мира собраны в CLDR (Unicode Common Locale Data Repository) – базе локалей, которой пользуются операционные системы, браузеры и i18n-библиотеки. Для русского CLDR определяет четыре категории. Выбор зависит от последней цифры целой части числа (i % 10), двух последних цифр (i % 100) и наличия дробной части (v – число знаков после запятой).
| Категория | Условие CLDR | Примеры | Форма |
|---|---|---|---|
| one | i % 10 = 1 и i % 100 ≠ 11, v = 0 | 1, 21, 31, 101, 1001 | товар |
| few | i % 10 = 2..4 и i % 100 ≠ 12..14, v = 0 | 2–4, 22–24, 102 | товара |
| many | i % 10 = 0, i % 10 = 5..9 или i % 100 = 11..14, v = 0 | 0, 5–19, 100, 1000 | товаров |
| other | дробные значения (v > 0) | 0,5; 1,5; 10,0 | товара |
Классическая самописная функция
Рунет с 2011 года решает задачу одной и той же функцией, кочующей по сниппетам и Q&A: сначала отсекается исключение 11–19, затем последняя цифра выбирает один из трёх элементов массива форм.
function numWord(value, words) {
value = Math.abs(value) % 100;
const num = value % 10;
if (value > 10 && value < 20) return words[2]; // 11–19 → товаров
if (num > 1 && num < 5) return words[1]; // 2–4 → товара
if (num === 1) return words[0]; // 1 → товар
return words[2]; // 0, 5–9 → товаров
}
numWord(1, ['товар', 'товара', 'товаров']); // 'товар'
numWord(21, ['товар', 'товара', 'товаров']); // 'товар'
numWord(11, ['товар', 'товара', 'товаров']); // 'товаров'Intl.PluralRules и самописная функция – в чём разница
Писать эти правила руками больше не нужно: в JavaScript есть стандартный API Intl.PluralRules. Конструктор принимает локаль, метод select(n) возвращает одну из категорий CLDR (zero/one/two/few/many/other) – остаётся сопоставить её со словарём форм.
const pr = new Intl.PluralRules('ru');
pr.select(1); // 'one'
pr.select(2); // 'few'
pr.select(5); // 'many'
pr.select(21); // 'one'
pr.select(11); // 'many'API входит в Baseline Widely available с сентября 2019 года: Chrome 63+, Firefox 58+, Safari 13+, Edge 79+, а на сервере – Node.js 10+, Deno и Bun; по caniuse глобальная поддержка около 94%, полифилл для современных таргетов не нужен.
| Критерий | Самописная функция | Intl.PluralRules |
|---|---|---|
| Языки | только русский и близкие славянские: в польском 21 склоняется как 5, а не как 1 | любая локаль CLDR, язык меняется аргументом конструктора |
| Правила | зашиты в код проекта, поддерживать самому | встроены в движок, не попадают в бандл |
| Дроби | обычно не учтены | отдельная категория other |
| Порядковые | ещё одна функция | опция { type: 'ordinal' } (1-й, 2-й) |
Отдельная ловушка – дробные числа. У русского категория other покрывает именно дроби: select(1.5) возвращает 'other' («полтора товара» – 1,5 товара). При этом other – единственная категория, гарантированная во всех локалях, поэтому ключ other в словаре форм обязателен: без него на первой же дроби интерфейс выведет undefined.
Зачем это нужно
Плюрализация – место, где качество интерфейса видно сразу: строка «5 товар(ов)» выдаёт сделанный наспех сайт не хуже битой вёрстки.
- Чистый текст интерфейса – без костылей вида «товар(ов)» и «дней: 5»; счётчики, таймеры и уведомления читаются как нормальный русский язык.
- Мультиязычность – «славянская» формула не переносится на другие языки, а Intl.PluralRules переключается на любой язык сменой локали, код не меняется.
- Меньше кода – правила CLDR уже встроены в браузер и Node.js, их не нужно тащить в бандл и обновлять при изменениях.
Если проект использует i18n-библиотеку, логика уже внутри: i18next, FormatJS/react-intl и Lingui сами вызывают Intl.PluralRules. В i18next (формат JSON v4, с версии 21.0.0) формы задаются суффиксами ключей _one, _few, _many, _other, а число передаётся обязательным параметром count:
{
"product_one": "{{count}} товар",
"product_few": "{{count}} товара",
"product_many": "{{count}} товаров",
"product_other": "{{count}} товара"
}
i18next.t('product', { count: 5 }); // '5 товаров'Пример
Готовый сниппет для счётчика товаров: словарь форм плюс функция из трёх строк. Экземпляр Intl.PluralRules создаётся один раз и переиспользуется.
const forms = { one: 'товар', few: 'товара', many: 'товаров', other: 'товара' };
const pr = new Intl.PluralRules('ru');
const label = (n) => `${n} ${forms[pr.select(n)]}`;
label(1); // '1 товар'
label(3); // '3 товара'
label(25); // '25 товаров'
label(11); // '11 товаров'
label(21); // '21 товар'
label(1.5); // '1.5 товара'Тот же словарь работает для любых сущностей – дней, комментариев, отзывов: меняются слова, логика остаётся. Для диапазонов («1–3 товара») у API есть метод selectRange(), а число с разделителями тысяч форматирует парный Intl.NumberFormat. На Битрикс-сайтах такой сниппет закрывает типовые места: счётчик корзины в шапке, «Найдено N товаров» в фильтре каталога, количество отзывов в карточке – включая счётчики, которые обновляются без перезагрузки страницы через делегирование событий.