Как выбрать форму множественного числа для диапазонов, например 1–3 предмета
Используйте JavaScript, чтобы правильно выбирать форму множественного числа при отображении числовых диапазонов
Введение
Диапазоны показывают, что значение находится между двумя границами. В интерфейсах это встречается, например, в результатах поиска — «Найдено 10–15 совпадений», в системах учёта — «Доступно 1–3 предмета», или в фильтрах — «Выберите 2–5 вариантов». В таких случаях два числа объединяются с описательным текстом, который должен грамматически согласовываться с диапазоном.
Когда вы показываете одно число, выбирается между единственным и множественным числом: «1 предмет» или «2 предмета». В разных языках свои правила выбора формы. В английском — единственное число для единицы и множественное для всех остальных. В польском — отдельные формы для 1, 2–4 и 5 и более. В арабском — шесть разных форм в зависимости от числа.
Диапазоны создают другую задачу. Форма множественного числа зависит не только от одного числа, а от начала и конца диапазона. В английском, например, «1–2 items» — используется множественное число, даже если диапазон начинается с 1. В разных языках свои правила для выбора формы в диапазоне. Метод selectRange() у Intl.PluralRules автоматически учитывает эти языковые особенности.
Почему для диапазонов нужны отдельные правила склонения
Использование метода select() только для одного числа из диапазона не всегда даёт правильный результат для всех языков. Можно подумать, что достаточно взять конечное значение диапазона, но во многих языках это приведёт к ошибке.
Например, в английском диапазон 0–1. Если применить select() к конечному значению, получится «one», и вы можете вывести «0–1 item». Но это грамматически неверно. Правильно — «0–1 items», то есть во множественном числе.
const rules = new Intl.PluralRules("en-US");
console.log(rules.select(1));
// Output: "one"
// But "0-1 item" is incorrect
// Correct: "0-1 items"
В разных языках существуют чёткие правила для диапазонов, которые не совпадают с правилами для отдельных чисел. Например, в словенском диапазон 102–201 использует форму "few", хотя отдельные числа в этом диапазоне требуют разных форм.
const slRules = new Intl.PluralRules("sl");
console.log(slRules.select(102));
// Output: "few"
console.log(slRules.select(201));
// Output: "few"
console.log(slRules.selectRange(102, 201));
// Output: "few"
В одних языках форма определяется по началу диапазона, в других — по концу, а в некоторых учитываются оба значения. Метод selectRange() инкапсулирует эти языковые правила, чтобы вам не пришлось реализовывать их вручную.
Создание экземпляра PluralRules для диапазонов
Создайте экземпляр Intl.PluralRules так же, как и для отдельных чисел. Экземпляр предоставляет как select() для отдельных чисел, так и selectRange() для диапазонов.
const rules = new Intl.PluralRules("en-US");
Вы можете указать опции при создании экземпляра. Эти опции применяются как к отдельным числам, так и к диапазонам.
const rules = new Intl.PluralRules("en-US", {
type: "cardinal"
});
Опция type по умолчанию установлена в "cardinal", что подходит для подсчёта объектов. Также можно использовать "ordinal" для порядковых чисел, хотя диапазоны порядковых чисел редко встречаются в интерфейсах.
Используйте один и тот же экземпляр для нескольких вызовов. Создавать новый экземпляр для каждой операции — неэффективно. Лучше сохранить его в переменной или кэшировать по локали.
Использование selectRange для определения категории во множественном числе для диапазонов
Метод selectRange() принимает два числа — начало и конец диапазона. Он возвращает строку с категорией во множественном числе: "zero", "one", "two", "few", "many" или "other".
const rules = new Intl.PluralRules("en-US");
console.log(rules.selectRange(0, 1));
// Output: "other"
console.log(rules.selectRange(1, 2));
// Output: "other"
console.log(rules.selectRange(5, 10));
// Output: "other"
В английском для диапазонов почти всегда используется категория "other", которая соответствует множественной форме. Это совпадает с тем, как носители английского обычно выражают диапазоны с существительными во множественном числе.
Языки с большим количеством форм множественного числа возвращают разные категории в зависимости от своих правил.
const arRules = new Intl.PluralRules("ar-EG");
console.log(arRules.selectRange(0, 0));
// Output: "zero"
console.log(arRules.selectRange(1, 1));
// Output: "one"
console.log(arRules.selectRange(2, 2));
// Output: "two"
console.log(arRules.selectRange(3, 10));
// Output: "few"
Возвращаемое значение всегда является одним из шести стандартных названий категорий множественного числа. Ваш код сопоставляет эти категории с соответствующим локализованным текстом.
Свяжите категории диапазонов с локализованными строками
Храните текстовые формы для каждой категории множественного числа в структуре данных. Используйте категорию, возвращённую selectRange(), чтобы найти нужный текст.
const rules = new Intl.PluralRules("en-US");
const forms = new Map([
["one", "item"],
["other", "items"]
]);
function formatRange(start, end) {
const category = rules.selectRange(start, end);
const form = forms.get(category);
return `${start}-${end} ${form}`;
}
console.log(formatRange(1, 3));
// Output: "1-3 items"
console.log(formatRange(0, 1));
// Output: "0-1 items"
console.log(formatRange(5, 10));
// Output: "5-10 items"
Этот подход разделяет логику выбора формы множественного числа и локализованный текст. Экземпляр Intl.PluralRules отвечает за языковые правила. Map хранит переводы. Функция объединяет их.
Для языков с большим количеством категорий множественного числа добавьте записи для каждой используемой категории.
const arRules = new Intl.PluralRules("ar-EG");
const arForms = new Map([
["zero", "عناصر"],
["one", "عنصر"],
["two", "عنصران"],
["few", "عناصر"],
["many", "عنصرًا"],
["other", "عنصر"]
]);
function formatRange(start, end) {
const category = arRules.selectRange(start, end);
const form = arForms.get(category);
return `${start}-${end} ${form}`;
}
console.log(formatRange(0, 0));
// Output: "0-0 عناصر"
console.log(formatRange(1, 1));
// Output: "1-1 عنصر"
Всегда добавляйте текст для каждой категории, используемой в языке. Проверьте правила множественного числа Unicode CLDR или протестируйте API на разных диапазонах, чтобы определить, какие категории нужны.
Как разные локали обрабатывают диапазоны во множественном числе
В каждом языке свои правила для определения формы множественного числа в диапазонах. Эти правила отражают, как носители языка естественно выражают диапазоны.
const enRules = new Intl.PluralRules("en-US");
console.log(enRules.selectRange(1, 3));
// Output: "other"
const slRules = new Intl.PluralRules("sl");
console.log(slRules.selectRange(102, 201));
// Output: "few"
const ptRules = new Intl.PluralRules("pt");
console.log(ptRules.selectRange(102, 102));
// Output: "other"
const ruRules = new Intl.PluralRules("ru");
console.log(ruRules.selectRange(1, 2));
// Output: "few"
В английском для диапазонов всегда используется "other", то есть диапазоны всегда во множественном числе. В словенском применяются более сложные правила, зависящие от конкретных чисел в диапазоне. В португальском для большинства диапазонов используется "other". В русском для некоторых диапазонов применяется "few".
Эти различия показывают, почему жёстко заданная логика множественного числа не подходит для интернациональных приложений. API содержит знания о том, как каждый язык работает с диапазонами.
Используйте вместе с Intl.NumberFormat для полного форматирования
В реальных приложениях нужно форматировать и числа, и текст. Используйте Intl.NumberFormat для форматирования концов диапазона по правилам локали, а затем selectRange() для выбора правильной формы множественного числа.
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 formatRange(start, end) {
const startFormatted = numberFormat.format(start);
const endFormatted = numberFormat.format(end);
const category = pluralRules.selectRange(start, end);
const form = forms.get(category);
return `${startFormatted}-${endFormatted} ${form}`;
}
console.log(formatRange(1, 3));
// Output: "1-3 items"
console.log(formatRange(1000, 5000));
// Output: "1,000-5,000 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 formatRange(start, end) {
const startFormatted = numberFormat.format(start);
const endFormatted = numberFormat.format(end);
const category = pluralRules.selectRange(start, end);
const form = forms.get(category);
return `${startFormatted}-${endFormatted} ${form}`;
}
console.log(formatRange(1000, 5000));
// Output: "1.000-5.000 Artikel"
В немецком языке в качестве разделителя тысяч используются точки, а не запятые. Форматтер чисел делает это автоматически. Правила множественного числа определяют, какую форму слова «Artikel» использовать.
Сравнение selectRange и select для одиночных значений
Метод select() работает с одиночными значениями, а selectRange() — с диапазонами. Используйте select(), чтобы показывать одно количество, и selectRange() — для отображения диапазона между двумя значениями.
const rules = new Intl.PluralRules("en-US");
// Single count
console.log(rules.select(1));
// Output: "one"
console.log(rules.select(2));
// Output: "other"
// Range
console.log(rules.selectRange(1, 2));
// Output: "other"
console.log(rules.selectRange(0, 1));
// Output: "other"
Для одиночных значений правила зависят только от этого числа. Для диапазонов правила учитывают оба конца. В английском языке диапазон, начинающийся с 1, всё равно использует форму во множественном числе, хотя для одного значения 1 используется единственное число.
В некоторых языках различия между правилами для одиночных значений и диапазонов ещё более заметны.
const slRules = new Intl.PluralRules("sl");
// Single counts in Slovenian
console.log(slRules.select(1));
// Output: "one"
console.log(slRules.select(2));
// Output: "two"
console.log(slRules.select(5));
// Output: "few"
// Range in Slovenian
console.log(slRules.selectRange(102, 201));
// Output: "few"
В словенском языке для разных одиночных значений используются формы «one», «two» и «few» по сложным правилам. Для диапазонов применяется другая логика, которая учитывает оба числа вместе.
Как обрабатывать диапазоны, где начало и конец совпадают
Когда начальное и конечное значения совпадают, вы отображаете диапазон с нулевой шириной. Некоторые приложения используют это, чтобы показать точное значение в контексте, где обычно ожидается диапазон.
const rules = new Intl.PluralRules("en-US");
console.log(rules.selectRange(5, 5));
// Output: "other"
console.log(rules.selectRange(1, 1));
// Output: "one"
Если оба значения равны 1, английский возвращает «one», что подразумевает использование единственного числа. Если оба значения равны любому другому числу, английский возвращает «other», что подразумевает форму во множественном числе.
Такое поведение логично, если вы отображаете диапазон как «1–1 элемент» или просто «1 элемент». Для других значений вы показываете «5–5 элементов» или «5 элементов».
На практике может быть удобнее определять, когда начало и конец совпадают, и показывать одно значение вместо диапазона.
const rules = new Intl.PluralRules("en-US");
const forms = new Map([
["one", "item"],
["other", "items"]
]);
function formatRange(start, end) {
if (start === end) {
const category = rules.select(start);
const form = forms.get(category);
return `${start} ${form}`;
}
const category = rules.selectRange(start, end);
const form = forms.get(category);
return `${start}-${end} ${form}`;
}
console.log(formatRange(1, 1));
// Output: "1 item"
console.log(formatRange(5, 5));
// Output: "5 items"
console.log(formatRange(1, 3));
// Output: "1-3 items"
В этом подходе используется select() для одинаковых значений и selectRange() для реальных диапазонов. Такой вывод выглядит естественнее, потому что не отображает «1–1» или «5–5».
Обработка крайних случаев с помощью selectRange
Метод selectRange() проверяет свои входные данные. Если любой из параметров — это undefined, null или не может быть преобразован в корректное число, метод выбрасывает ошибку.
const rules = new Intl.PluralRules("en-US");
try {
console.log(rules.selectRange(1, undefined));
} catch (error) {
console.log(error.name);
// Output: "TypeError"
}
try {
console.log(rules.selectRange(NaN, 5));
} catch (error) {
console.log(error.name);
// Output: "RangeError"
}
Проверьте входные данные перед передачей их в selectRange(). Это особенно важно при работе с пользовательским вводом или данными из внешних источников.
function formatRange(start, end) {
if (typeof start !== "number" || typeof end !== "number") {
throw new Error("Start and end must be numbers");
}
if (isNaN(start) || isNaN(end)) {
throw new Error("Start and end must be valid numbers");
}
const category = rules.selectRange(start, end);
const form = forms.get(category);
return `${start}-${end} ${form}`;
}
Метод принимает числа, значения BigInt или строки, которые можно преобразовать в числа.
const rules = new Intl.PluralRules("en-US");
console.log(rules.selectRange(1, 5));
// Output: "other"
console.log(rules.selectRange(1n, 5n));
// Output: "other"
console.log(rules.selectRange("1", "5"));
// Output: "other"
Строковые значения преобразуются в числа. Это даёт гибкость при вызове метода, но для ясности лучше передавать именно числа, если есть такая возможность.
Обработка диапазонов с десятичными числами
Метод selectRange() работает с десятичными числами. Это удобно для отображения диапазонов дробных величин, например, измерений или статистики.
const rules = new Intl.PluralRules("en-US");
console.log(rules.selectRange(1.5, 2.5));
// Output: "other"
console.log(rules.selectRange(0.5, 1.0));
// Output: "other"
console.log(rules.selectRange(1.0, 1.5));
// Output: "other"
В английском все такие диапазоны с десятичными считаются во множественном числе. В других языках могут быть свои правила для десятичных диапазонов.
При форматировании диапазонов с десятичными числами объединяйте selectRange() с Intl.NumberFormat, настроенным на нужную точность.
const locale = "en-US";
const numberFormat = new Intl.NumberFormat(locale, {
minimumFractionDigits: 1,
maximumFractionDigits: 1
});
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
["one", "kilometer"],
["other", "kilometers"]
]);
function formatRange(start, end) {
const startFormatted = numberFormat.format(start);
const endFormatted = numberFormat.format(end);
const category = pluralRules.selectRange(start, end);
const form = forms.get(category);
return `${startFormatted}-${endFormatted} ${form}`;
}
console.log(formatRange(1.5, 2.5));
// Output: "1.5-2.5 kilometers"
console.log(formatRange(0.5, 1.0));
// Output: "0.5-1.0 kilometers"
Форматтер чисел обеспечивает единообразное отображение десятичных дробей. Правила множественного числа определяют правильную форму в зависимости от значения после запятой.
Поддержка браузеров и совместимость
Метод selectRange() относительно новый по сравнению с остальной частью Intl API. Он стал доступен в 2023 году как часть спецификации Intl.NumberFormat v3.
Поддержка браузеров включает Chrome 106 и новее, Firefox 116 и новее, Safari 15.4 и новее, а также Edge 106 и новее. Этот метод недоступен в Internet Explorer и старых версиях браузеров.
Если ваше приложение ориентировано на современные браузеры, вы можете использовать selectRange() без polyfill. Если нужна поддержка старых браузеров, проверьте наличие метода перед его использованием.
const rules = new Intl.PluralRules("en-US");
if (typeof rules.selectRange === "function") {
// Use selectRange for range pluralization
console.log(rules.selectRange(1, 3));
} else {
// Fall back to select with the end value
console.log(rules.select(3));
}
Этот запасной вариант использует select() для конечного значения, если selectRange() недоступен. Это не идеально с лингвистической точки зрения для всех языков, но даёт приемлемое приближение для старых браузеров.
Polyfill-доступны через такие пакеты, как @formatjs/intl-pluralrules, если вам нужна полная поддержка старых сред.
Когда использовать selectRange вместо select
Используйте selectRange(), когда ваш интерфейс явно показывает диапазон с видимыми для пользователя начальным и конечным значениями. Это подходит для таких случаев, как результаты поиска с "Найдено 10–15 совпадений", склад с "1–3 товара в наличии" или фильтры с "Выберите 2–5 вариантов".
Используйте select(), когда отображается одно число, даже если оно приблизительное или суммарное. Например, "Около 10 результатов" использует select(10), потому что показывается одно число, а не диапазон.
Если ваш диапазон отображается с помощью Intl.NumberFormat.formatRange() для чисел, используйте selectRange() для сопроводительного текста. Это обеспечит согласованность между форматированием чисел и склонением текста по числам.
const locale = "en-US";
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
["one", "result"],
["other", "results"]
]);
function formatSearchResults(start, end) {
const rangeFormatted = numberFormat.formatRange(start, end);
const category = pluralRules.selectRange(start, end);
const form = forms.get(category);
return `Found ${rangeFormatted} ${form}`;
}
console.log(formatSearchResults(10, 15));
// Output: "Found 10–15 results"
В этом шаблоне используется formatRange() из Intl.NumberFormat для форматирования чисел и selectRange() из Intl.PluralRules для выбора текста. Оба метода работают с диапазонами, обеспечивая корректную обработку для всех языков.