Как выбрать форму множественного числа для диапазонов, таких как 1–3 элемента
Используйте JavaScript для выбора правильной формы множественного числа при отображении диапазонов чисел
Введение
Диапазоны показывают, что значение находится между двумя конечными точками. Пользовательские интерфейсы отображают диапазоны в таких контекстах, как результаты поиска с надписью "Найдено 10–15 совпадений", системы инвентаризации с надписью "Доступно 1–3 предмета" или фильтры с надписью "Выберите 2–5 вариантов". Эти диапазоны объединяют два числа с описательным текстом, который должен грамматически согласовываться с диапазоном.
Когда вы отображаете одно количество, вы выбираете между единственным и множественным числом: "1 предмет" против "2 предмета". В языках существуют правила, определяющие, какая форма применяется в зависимости от количества. Эти правила различаются в зависимости от языка. В английском языке используется единственное число для одного и множественное для всех остальных количеств. В польском языке используются разные формы для 1, 2–4 и 5 или более. В арабском языке существует шесть различных форм в зависимости от количества.
Диапазоны представляют собой другую задачу. Множественная форма зависит как от начального, так и от конечного значения, а не только от одного числа. В английском языке "1–2 items" (1–2 предмета) используется множественное число, даже если диапазон начинается с 1. В разных языках существуют разные правила для определения, какая множественная форма применяется к диапазону. Метод selectRange() в Intl.PluralRules автоматически обрабатывает эти языковые правила.
Почему диапазоны требуют других правил множественного числа
Использование метода select() для одного числа из диапазона не работает корректно для всех языков. Вы можете подумать, что нужно использовать конечное значение диапазона, но это приводит к неправильным результатам во многих языках.
Рассмотрим английский язык с диапазоном 0–1. Использование select() для конечного значения возвращает "one" (один), предполагая, что вы должны отобразить "0–1 item" (0–1 предмет). Это грамматически неверно. Правильная форма — "0–1 items" (0–1 предмета) с множественным числом.
const rules = new Intl.PluralRules("en-US");
console.log(rules.select(1));
// Вывод: "one"
// Но "0–1 item" неверно
// Правильно: "0–1 items"
В разных языках существуют явные правила для диапазонов, которые не совпадают с их правилами для отдельных количеств. В словенском языке диапазон 102–201 использует форму "few" (несколько), в то время как отдельные числа в этом диапазоне используют разные формы.
const slRules = new Intl.PluralRules("sl");
console.log(slRules.select(102));
// Вывод: "few"
console.log(slRules.select(201));
// Вывод: "few"
console.log(slRules.selectRange(102, 201));
// Вывод: "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));
// Вывод: "other"
console.log(rules.selectRange(1, 2));
// Вывод: "other"
console.log(rules.selectRange(5, 10));
// Вывод: "other"
В английском языке диапазоны почти всегда используют категорию "other", которая соответствует множественной форме. Это соответствует тому, как носители английского языка естественно выражают диапазоны с использованием множественных существительных.
Языки с большим количеством форм множественного числа возвращают разные категории в зависимости от их специфических правил.
const arRules = new Intl.PluralRules("ar-EG");
console.log(arRules.selectRange(0, 0));
// Вывод: "zero"
console.log(arRules.selectRange(1, 1));
// Вывод: "one"
console.log(arRules.selectRange(2, 2));
// Вывод: "two"
console.log(arRules.selectRange(3, 10));
// Вывод: "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));
// Вывод: "1-3 items"
console.log(formatRange(0, 1));
// Вывод: "0-1 items"
console.log(formatRange(5, 10));
// Вывод: "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));
// Вывод: "0-0 عناصر"
console.log(formatRange(1, 1));
// Вывод: "1-1 عنصر"
Всегда предоставляйте текст для каждой категории, используемой языком. Проверьте правила множественного числа Unicode CLDR или протестируйте API на разных диапазонах, чтобы определить, какие категории необходимы.
Как разные локали обрабатывают множественное число для диапазонов
Каждый язык имеет свои правила для определения формы множественного числа для диапазонов. Эти правила отражают, как носители языка естественно выражают диапазоны на этом языке.
const enRules = new Intl.PluralRules("en-US");
console.log(enRules.selectRange(1, 3));
// Вывод: "other"
const slRules = new Intl.PluralRules("sl");
console.log(slRules.selectRange(102, 201));
// Вывод: "few"
const ptRules = new Intl.PluralRules("pt");
console.log(ptRules.selectRange(102, 102));
// Вывод: "other"
const ruRules = new Intl.PluralRules("ru");
console.log(ruRules.selectRange(1, 2));
// Вывод: "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));
// Вывод: "1-3 items"
console.log(formatRange(1000, 5000));
// Вывод: "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));
// Вывод: "1.000-5.000 Artikel"
В немецком языке точки используются как разделители тысяч вместо запятых. Форматировщик чисел обрабатывает это автоматически. Правила множественного числа определяют, какую форму "Artikel" использовать.
Сравнение selectRange с select для одиночных значений
Метод select() обрабатывает одиночные значения, а selectRange() — диапазоны. Используйте select() для отображения одного количества и selectRange() для отображения диапазона между двумя значениями.
const rules = new Intl.PluralRules("en-US");
// Одиночное значение
console.log(rules.select(1));
// Вывод: "one"
console.log(rules.select(2));
// Вывод: "other"
// Диапазон
console.log(rules.selectRange(1, 2));
// Вывод: "other"
console.log(rules.selectRange(0, 1));
// Вывод: "other"
Для одиночных значений правила зависят только от этого числа. Для диапазонов правила учитывают оба значения. В английском языке диапазон, начинающийся с 1, всё равно использует форму множественного числа, даже если одиночное значение 1 использует форму единственного числа.
Некоторые языки показывают более заметные различия между правилами для одиночных значений и диапазонов.
const slRules = new Intl.PluralRules("sl");
// Одиночные значения в словенском
console.log(slRules.select(1));
// Вывод: "one"
console.log(slRules.select(2));
// Вывод: "two"
console.log(slRules.select(5));
// Вывод: "few"
// Диапазон в словенском
console.log(slRules.selectRange(102, 201));
// Вывод: "few"
В словенском языке используются "one", "two" и "few" для разных одиночных значений на основе сложных правил. Для диапазонов применяется другая логика, которая учитывает оба числа.
Обработка диапазонов, где начальное и конечное значения равны
Когда начальное и конечное значения совпадают, вы отображаете диапазон с нулевой шириной. Некоторые приложения используют это для представления точного значения в контексте, где ожидаются диапазоны.
const rules = new Intl.PluralRules("en-US");
console.log(rules.selectRange(5, 5));
// Вывод: "other"
console.log(rules.selectRange(1, 1));
// Вывод: "one"
Когда оба значения равны 1, английский язык возвращает "one", что предполагает использование единственного числа. Когда оба значения равны любому другому числу, английский язык возвращает "other", что предполагает использование множественного числа.
Такое поведение логично, если вы отображаете диапазон как "1-1 item" или просто "1 item". Для значений, отличных от 1, вы отображаете "5-5 items" или "5 items".
На практике вы можете захотеть обнаруживать, когда начальное значение равно конечному, и отображать одно значение вместо диапазона.
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));
// Вывод: "1 item"
console.log(formatRange(5, 5));
// Вывод: "5 items"
console.log(formatRange(1, 3));
// Вывод: "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);
// Вывод: "TypeError"
}
try {
console.log(rules.selectRange(NaN, 5));
} catch (error) {
console.log(error.name);
// Вывод: "RangeError"
}
Проверяйте свои входные данные перед передачей их в selectRange(). Это особенно важно при работе с пользовательским вводом или данными из внешних источников.
function formatRange(start, end) {
if (typeof start !== "number" || typeof end !== "number") {
throw new Error("Начальное и конечное значения должны быть числами");
}
if (isNaN(start) || isNaN(end)) {
throw new Error("Начальное и конечное значения должны быть допустимыми числами");
}
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));
// Вывод: "other"
console.log(rules.selectRange(1n, 5n));
// Вывод: "other"
console.log(rules.selectRange("1", "5"));
// Вывод: "other"
Строковые входные данные преобразуются в числа. Это обеспечивает гибкость в использовании метода, но для ясности рекомендуется передавать фактические числовые типы.
Обработка диапазонов с десятичными числами
Метод selectRange() работает с десятичными числами. Это полезно при отображении диапазонов дробных величин, таких как измерения или статистика.
const rules = new Intl.PluralRules("en-US");
console.log(rules.selectRange(1.5, 2.5));
// Вывод: "other"
console.log(rules.selectRange(0.5, 1.0));
// Вывод: "other"
console.log(rules.selectRange(1.0, 1.5));
// Вывод: "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", "километр"],
["other", "километры"]
]);
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));
// Вывод: "1.5-2.5 километры"
console.log(formatRange(0.5, 1.0));
// Вывод: "0.5-1.0 километры"
Форматирование чисел обеспечивает единообразное отображение десятичных чисел. Правила множественного числа определяют правильную форму на основе десятичных значений.
Поддержка браузерами и совместимость
Метод selectRange() относительно новый по сравнению с остальной частью API Intl. Он стал доступен в 2023 году в рамках спецификации Intl.NumberFormat v3.
Поддержка браузерами включает Chrome 106 и новее, Firefox 116 и новее, Safari 15.4 и новее, а также Edge 106 и новее. Метод недоступен в Internet Explorer или более старых версиях браузеров.
Для приложений, ориентированных на современные браузеры, вы можете использовать selectRange() без полифила. Если вам нужно поддерживать старые браузеры, проверьте наличие метода перед его использованием.
const rules = new Intl.PluralRules("en-US");
if (typeof rules.selectRange === "function") {
// Используйте selectRange для множественного числа диапазонов
console.log(rules.selectRange(1, 3));
} else {
// Используйте select для конечного значения
console.log(rules.select(3));
}
Этот подход с резервным вариантом использует select() для конечного значения, если selectRange() недоступен. Это не идеально с лингвистической точки зрения для всех языков, но обеспечивает разумное приближение для старых браузеров.
Полифилы доступны через такие пакеты, как @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", "результат"],
["other", "результатов"]
]);
function formatSearchResults(start, end) {
const rangeFormatted = numberFormat.formatRange(start, end);
const category = pluralRules.selectRange(start, end);
const form = forms.get(category);
return `Найдено ${rangeFormatted} ${form}`;
}
console.log(formatSearchResults(10, 15));
// Вывод: "Найдено 10–15 результатов"
Этот шаблон использует formatRange() из Intl.NumberFormat для форматирования чисел и selectRange() из Intl.PluralRules для выбора текста. Оба метода работают с диапазонами, обеспечивая корректную обработку для всех языков.