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. Шесть категорий: ноль, один, два, несколько, много и другие. Не каждый язык использует все шесть категорий. Английский использует только "один" и "другие". Арабский использует все шесть.
Создание экземпляра PluralRules для локали
Конструктор Intl.PluralRules принимает идентификатор локали и возвращает объект, который может определить, к какой категории множественного числа относится заданное число.
const enRules = new Intl.PluralRules('en-US');
Создавайте один экземпляр на локаль и переиспользуйте его. Создание нового экземпляра для каждой операции множественного числа неэффективно. Сохраните экземпляр в переменной или используйте механизм кэширования.
Тип по умолчанию — кардинальный, который используется для подсчета объектов. Вы также можете создать правила для порядковых чисел, передав объект с опциями.
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}`;
}
Всегда предоставляйте записи для каждой категории, используемой языком. Отсутствие категорий приводит к неопределенным результатам. Если вы не уверены, какие категории использует язык, проверьте правила множественного числа 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'
Ваш текст должен учитывать это. Для английского языка вы можете захотеть показывать "No items" вместо "0 items" для улучшения 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 включает разбор локали и загрузку данных о правилах множественного числа. Делайте это один раз для каждой локали, а не при каждом вызове функции или цикле рендеринга.
// Хорошо: создайте один раз, используйте повторно
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 без полифила. Если вам нужно поддерживать старые браузеры, полифилы доступны через такие пакеты, как intl-pluralrules на npm.
Метод selectRange() является более новым и имеет немного более ограниченную поддержку. Он доступен в Chrome 106+, Firefox 116+, Safari 15.4+ и Edge 106+. Проверьте совместимость, если вы используете selectRange() и вам нужно поддерживать старые версии браузеров.
Избегайте жесткого кодирования форм множественного числа в логике
Не проверяйте количество и не используйте ветвление в коде для выбора формы множественного числа. Этот подход не масштабируется для языков с более чем двумя формами и привязывает вашу логику к правилам английского языка.
// Избегайте этого подхода
function formatItems(count) {
if (count === 1) {
return `${count} item`;
}
return `${count} items`;
}
Используйте Intl.PluralRules и структуру данных для хранения форм. Это делает ваш код независимым от языка и упрощает добавление новых языков, предоставляя новые переводы.
// Предпочитайте этот подход
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}`;
}
Этот подход работает одинаково для любого языка. Меняются только экземпляр rules и Map форм.
Тестируйте с несколькими локалями и граничными случаями
Правила множественного числа имеют граничные случаи, которые легко упустить, если тестировать только на английском языке. Тестируйте вашу логику множественного числа хотя бы на одном языке, который использует более двух форм, например, польском или арабском.
Тестируйте количества, которые активируют разные категории:
- Ноль
- Один
- Два
- Несколько (3-10 в арабском)
- Много (11-99 в арабском)
- Большие числа (100+)
- Десятичные значения (0.5, 1.5, 2.3)
- Отрицательные числа, если ваш интерфейс их отображает
Если вы используете порядковые правила, тестируйте числа, которые активируют разные суффиксы: 1, 2, 3, 4, 11, 21, 22, 23. Это гарантирует, что вы правильно обрабатываете особые случаи.
Тестирование с несколькими локалями на ранних этапах предотвращает сюрпризы при добавлении новых языков позже. Это также подтверждает, что ваша структура данных включает все необходимые категории и что ваша логика правильно их обрабатывает.