API Intl.PluralRules
Как правильно обрабатывать формы множественного числа в JavaScript
Введение
Плюрализация — это процесс отображения разного текста в зависимости от количества. Например, на английском показывают «1 item» для одного элемента и «2 items» для нескольких. Обычно разработчики просто добавляют «s» к слову, если количество не равно одному.
Такой подход не работает для других языков. В польском используются разные формы для 1, 2–4 и 5 и более. В арабском есть формы для нуля, одного, двух, нескольких и многих. В валлийском — шесть разных форм. Даже в английском есть исключения, например, «person» и «people», которые требуют отдельной обработки.
API Intl.PluralRules решает эту задачу, предоставляя категорию формы множественного числа для любого числа на любом языке. Вы передаёте количество, а API сообщает, какую форму использовать по правилам нужного языка. Это позволяет писать код, готовый к интернационализации, который корректно работает для разных языков без ручного прописывания языковых правил.
Как языки обрабатывают формы множественного числа
Языки сильно различаются в выражении количества. В английском две формы: единственное число для одного, множественное — для всего остального. Это кажется простым, пока не столкнёшься с языками с другими системами.
В русском и польском три формы. Единственное число используется для одного предмета. Особая форма — для чисел, оканчивающихся на 2, 3 или 4 (но не 12, 13, 14). Для всех остальных чисел используется третья форма.
В арабском шесть форм: ноль, один, два, несколько (3–10), много (11–99) и прочие (100+). В валлийском тоже шесть форм, но с другими числовыми границами.
В некоторых языках, например, китайском и японском, вообще нет различия между единственным и множественным числом. Одна и та же форма используется для любого количества.
API Intl.PluralRules абстрагирует эти различия, используя стандартные имена категорий на основе правил множественного числа Unicode CLDR. Всего есть шесть категорий: zero, one, two, few, many и other. Не все языки используют все шесть категорий. В английском используются только one и other. В арабском — все шесть.
Создание экземпляра PluralRules для локали
Конструктор Intl.PluralRules принимает идентификатор локали и возвращает объект, который может определить, к какой категории множественного числа относится заданное число.
const enRules = new Intl.PluralRules('en-US');
Создавайте по одному экземпляру на каждую локаль и переиспользуйте его. Создавать новый экземпляр для каждой операции — неэффективно. Сохраняйте экземпляр в переменной или используйте кэширование.
Тип по умолчанию — cardinal, он используется для подсчёта объектов. Также можно создать правила для порядковых числительных, передав объект с опциями.
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
Кардинальные правила применяются к количеству, например: «1 яблоко, 2 яблока». Порядковые — к позициям, например: «1-е место, 2-е место».
Использование select() для получения категории числа
Метод select() принимает число и возвращает, к какой категории множественного числа оно относится в целевом языке.
const enRules = new Intl.PluralRules('en-US');
enRules.select(0); // 'other'
enRules.select(1); // 'one'
enRules.select(2); // 'other'
enRules.select(5); // 'other'
Возвращаемое значение — всегда одно из шести имён категорий: zero, one, two, few, many или other. В английском возвращаются только one и other, потому что только эти формы используются.
В арабском, где правила сложнее, используются все шесть категорий:
const arRules = new Intl.PluralRules('ar-EG');
arRules.select(0); // 'zero'
arRules.select(1); // 'one'
arRules.select(2); // 'two'
arRules.select(6); // 'few'
arRules.select(18); // 'many'
arRules.select(100); // 'other'
Связывайте категории с локализованными строками
API сообщает только, какая категория применяется. Текст для каждой категории вы задаёте сами. Храните текстовые формы в Map или объекте, где ключ — это имя категории.
const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
formatItems(1); // '1 item'
formatItems(5); // '5 items'
Такой подход разделяет логику и данные. Экземпляр PluralRules отвечает за правила, а Map — за переводы. Функция объединяет их.
Для языков с большим количеством категорий добавляйте больше элементов в Map:
const arRules = new Intl.PluralRules('ar-EG');
const arForms = new Map([
['zero', 'عناصر'],
['one', 'عنصر واحد'],
['two', 'عنصران'],
['few', 'عناصر'],
['many', 'عنصرًا'],
['other', 'عنصر'],
]);
function formatItems(count) {
const category = arRules.select(count);
const form = arForms.get(category);
return `${count} ${form}`;
}
Всегда добавляйте элементы для всех категорий, которые используются в языке. Если пропустить категорию, поиск по ключу вернёт undefined. Если не уверены, какие категории есть в языке, проверьте правила множественного числа Unicode CLDR или протестируйте API с разными числами.
Обработка десятичных и дробных чисел
Метод select() работает с десятичными числами. В английском языке десятичные значения считаются во множественном числе, даже если они между 0 и 2.
const enRules = new Intl.PluralRules('en-US');
enRules.select(1); // 'one'
enRules.select(1.0); // 'one'
enRules.select(1.5); // 'other'
enRules.select(0.5); // 'other'
В других языках правила для десятичных чисел отличаются. В некоторых любое десятичное число считается множественным, в других — используются более сложные правила, зависящие от дробной части.
Если в интерфейсе отображаются дробные значения, например, «1,5 ГБ» или «2,7 мили», передавайте дробное число напрямую в select(). Не округляйте заранее, если только интерфейс не округляет отображаемое значение.
Форматируйте порядковые номера: 1-й, 2-й, 3-й
Порядковые номера показывают позицию или ранг. В английском языке к числам добавляются суффиксы: 1st, 2nd, 3rd, 4th. Это не просто «добавить th», потому что 1, 2 и 3 имеют особые формы, а числа, заканчивающиеся на 1, 2 или 3, следуют особым правилам (21st, 22nd, 23rd), кроме случаев, когда они заканчиваются на 11, 12 или 13 (11th, 12th, 13th).
API Intl.PluralRules обрабатывает эти правила, когда вы указываете type: 'ordinal'.
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
enOrdinalRules.select(1); // 'one'
enOrdinalRules.select(2); // 'two'
enOrdinalRules.select(3); // 'few'
enOrdinalRules.select(4); // 'other'
enOrdinalRules.select(11); // 'other'
enOrdinalRules.select(21); // 'one'
enOrdinalRules.select(22); // 'two'
enOrdinalRules.select(23); // 'few'
Свяжите категории с порядковыми суффиксами:
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const enOrdinalSuffixes = new Map([
['one', 'st'],
['two', 'nd'],
['few', 'rd'],
['other', 'th'],
]);
function formatOrdinal(n) {
const category = enOrdinalRules.select(n);
const suffix = enOrdinalSuffixes.get(category);
return `${n}${suffix}`;
}
formatOrdinal(1); // '1st'
formatOrdinal(2); // '2nd'
formatOrdinal(3); // '3rd'
formatOrdinal(4); // '4th'
formatOrdinal(11); // '11th'
formatOrdinal(21); // '21st'
В других языках используются совершенно разные системы порядковых числительных. Например, во французском — «1er» для первого и «2e» для всех остальных. В испанском есть родовые различия в порядковых числительных. API возвращает категорию, а вы подставляете локализованные формы.
Обработка диапазонов с помощью selectRange()
Метод selectRange() определяет категорию множественного числа для диапазона чисел, например, «1–5 предметов» или «10–20 результатов». В некоторых языках для диапазонов действуют другие правила множественного числа, чем для отдельных чисел.
const enRules = new Intl.PluralRules('en-US');
enRules.selectRange(1, 5); // 'other'
enRules.selectRange(0, 1); // 'other'
В английском языке диапазоны почти всегда считаются во множественном числе, даже если диапазон начинается с 1. В других языках правила для диапазонов могут быть сложнее.
const slRules = new Intl.PluralRules('sl');
slRules.selectRange(102, 201); // 'few'
const ptRules = new Intl.PluralRules('pt');
ptRules.selectRange(102, 102); // 'other'
Используйте selectRange() для явного отображения диапазонов в интерфейсе. Для отдельных чисел используйте select().
Комбинируйте с Intl.NumberFormat для локализованного отображения чисел
Множественные формы часто используются вместе с форматированными числами. Сначала используйте Intl.NumberFormat для форматирования числа по правилам локали, затем Intl.PluralRules для выбора правильной формы текста.
const locale = 'en-US';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatCount(count) {
const formattedNumber = numberFormat.format(count);
const category = pluralRules.select(count);
const form = forms.get(category);
return `${formattedNumber} ${form}`;
}
formatCount(1); // '1 item'
formatCount(1000); // '1,000 items'
formatCount(1.5); // '1.5 items'
Например, в немецком языке точка используется как разделитель тысяч, а запятая — как десятичный разделитель:
const locale = 'de-DE';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
['one', 'Artikel'],
['other', 'Artikel'],
]);
function formatCount(count) {
const formattedNumber = numberFormat.format(count);
const category = pluralRules.select(count);
const form = forms.get(category);
return `${formattedNumber} ${form}`;
}
formatCount(1); // '1 Artikel'
formatCount(1000); // '1.000 Artikel'
formatCount(1.5); // '1,5 Artikel'
Такой подход гарантирует, что и формат числа, и форма текста будут соответствовать ожиданиям пользователей для выбранной локали.
Явно обрабатывайте случай с нулём, если это необходимо
Правила для нуля различаются в зависимости от языка. В английском обычно используется форма во множественном числе: «0 items», «0 results». В некоторых языках для нуля используется единственное число, а в других — отдельная категория для нуля.
API Intl.PluralRules возвращает подходящую категорию для нуля в зависимости от языковых правил. В английском для нуля возвращается 'other', что соответствует множественной форме:
const enRules = new Intl.PluralRules('en-US');
enRules.select(0); // 'other'
В арабском языке для нуля есть отдельная категория:
const arRules = new Intl.PluralRules('ar-EG');
arRules.select(0); // 'zero'
Ваш текст должен это учитывать. В английском, например, лучше показывать «Нет элементов» вместо «0 элементов» для лучшего UX. Обрабатывайте это до вызова правил множественного числа:
function formatItems(count) {
if (count === 0) {
return 'No items';
}
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
Для арабского добавьте отдельную форму для нуля в переводах:
const arForms = new Map([
['zero', 'لا توجد عناصر'],
['one', 'عنصر واحد'],
['two', 'عنصران'],
['few', 'عناصر'],
['many', 'عنصرًا'],
['other', 'عنصر'],
]);
Такой подход учитывает языковые особенности каждого языка и позволяет настраивать отображение нуля для лучшего пользовательского опыта.
Используйте экземпляры PluralRules повторно для производительности
Создание экземпляра PluralRules включает разбор локали и загрузку данных о правилах множественного числа. Делайте это один раз для каждой локали, а не при каждом вызове функции или рендере.
// Good: create once, reuse
const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
Если вы поддерживаете несколько локалей, создайте экземпляры для каждой и храните их в Map или кэше:
const rulesCache = new Map();
function getPluralRules(locale) {
if (!rulesCache.has(locale)) {
rulesCache.set(locale, new Intl.PluralRules(locale));
}
return rulesCache.get(locale);
}
const rules = getPluralRules('en-US');
Такой паттерн распределяет стоимость инициализации между многими вызовами.
Поддержка браузеров и совместимость
Intl.PluralRules поддерживается во всех современных браузерах с 2019 года. Включая Chrome 63+, Firefox 58+, Safari 13+ и Edge 79+. В Internet Explorer не поддерживается.
Для приложений, ориентированных на современные браузеры, можно использовать Intl.PluralRules без polyfill. Если нужно поддерживать старые браузеры, polyfill доступны через пакеты вроде intl-pluralrules на npm.
Метод selectRange() новее и поддерживается чуть менее широко. Он доступен в Chrome 106+, Firefox 116+, Safari 15.4+ и Edge 106+. Проверьте совместимость, если используете selectRange() и поддерживаете старые версии браузеров.
Не хардкодьте формы множественного числа в логике
Не проверяйте количество и не выбирайте форму во внутренней логике. Такой подход не масштабируется для языков с более чем двумя формами и привязывает вашу логику к английским правилам.
// Avoid this pattern
function formatItems(count) {
if (count === 1) {
return `${count} item`;
}
return `${count} items`;
}
Используйте Intl.PluralRules и структуру данных для хранения форм. Это делает ваш код независимым от языка и позволяет легко добавлять новые языки, просто предоставляя переводы.
// Prefer this pattern
const rules = new Intl.PluralRules('en-US');
const forms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = rules.select(count);
const form = forms.get(category);
return `${count} ${form}`;
}
Этот подход работает одинаково для любого языка. Меняются только экземпляр правил и Map с формами.
Тестируйте с разными локалями и граничными случаями
В правилах множественного числа есть граничные случаи, которые легко упустить, если тестировать только на английском. Проверьте свою логику склонения хотя бы на одном языке с более чем двумя формами, например, на польском или арабском.
Проверьте числа, которые попадают в разные категории:
- Ноль
- Один
- Два
- Несколько (3–10 в арабском)
- Много (11–99 в арабском)
- Большие числа (100+)
- Дробные значения (0,5; 1,5; 2,3)
- Отрицательные числа, если ваш интерфейс их показывает
Если вы используете порядковые правила, проверьте числа, которые вызывают разные суффиксы: 1, 2, 3, 4, 11, 21, 22, 23. Это поможет корректно обработать особые случаи.
Раннее тестирование с разными локалями помогает избежать сюрпризов при добавлении новых языков. Это также подтверждает, что ваша структура данных включает все нужные категории, а логика их правильно обрабатывает.