Как выбрать правильную форму множественного числа для разных языков?
Используйте Intl.PluralRules в JavaScript, чтобы выбирать между одной, двумя, несколькими и многими единицами в зависимости от языковых правил
Введение
Когда вы отображаете текст с количествами, для разных чисел нужны разные сообщения. По-английски пишут «1 file», но «2 files». Самый простой способ — просто соединить число со словом и добавить «s» при необходимости.
function formatFileCount(count) {
return count === 1 ? `${count} file` : `${count} files`;
}
У этого подхода есть три недостатка. Во-первых, он даёт некорректный английский для нуля ("0 files" логичнее было бы перевести как "no files"). Во-вторых, он не работает со сложными формами, например, "1 child, 2 children" или "1 person, 2 people". В-третьих, и это главное, в других языках свои правила образования множественного числа, которые этот код не учитывает.
JavaScript предоставляет Intl.PluralRules, чтобы решить эту проблему. Этот API определяет, какую форму множественного числа использовать для любого числа на любом языке, следуя стандарту Unicode CLDR, который используют профессиональные системы перевода по всему миру.
Почему для разных языков нужны разные формы множественного числа
В английском языке две формы множественного числа. Пишут «1 book» и «2 books». Слово меняется, если количество равно одному, и остаётся другим для всех остальных чисел.
В других языках всё иначе. В польском три формы по сложным правилам. В русском — четыре формы. В арабском — шесть. В некоторых языках для всех количеств используется только одна форма.
Вот примеры, как меняется слово «яблоко» в зависимости от количества в разных языках:
Английский: 1 apple, 2 apples, 5 apples, 0 apples
Польский: 1 jabłko, 2 jabłka, 5 jabłek, 0 jabłek
Русский: 1 яблоко, 2 яблока, 5 яблок, 0 яблок
Арабский: использует шесть разных форм в зависимости от того, ноль, один, два, несколько, много или другое количество
Unicode CLDR определяет точные правила, когда использовать каждую форму в каждом языке. Запомнить эти правила или захардкодить их в приложении невозможно. Вам нужен API, который знает их.
Что такое категории множественного числа в CLDR
Стандарт Unicode CLDR определяет шесть категорий множественного числа, которые охватывают все языки:
zero: используется в некоторых языках для ровно нуляone: используется для единственного числаtwo: используется в языках с двойственным числомfew: используется для малых количеств в некоторых языкахmany: используется для больших количеств или дробей в некоторых языкахother: форма по умолчанию, используется, если не подходит ни одна другая категория
Каждый язык использует категорию other. Большинство языков используют всего две или три категории. Категории не всегда напрямую связаны с количеством. Например, в польском число 5 относится к категории many, как и 0, 25 и 1,5.
Конкретные правила, какие числа относятся к каким категориям, различаются в зависимости от языка. JavaScript справляется с этой сложностью с помощью API Intl.PluralRules.
Как определить, какую форму использовать
Объект Intl.PluralRules определяет, к какой категории относится число в конкретном языке. Вы создаёте объект PluralRules с нужной локалью, а затем вызываете его метод select() с числом.
const rules = new Intl.PluralRules('en-US');
console.log(rules.select(0)); // "other"
console.log(rules.select(1)); // "one"
console.log(rules.select(2)); // "other"
console.log(rules.select(5)); // "other"
В английском select() возвращает "one" для числа 1 и "other" для всех остальных.
В польском три категории с более сложными правилами:
const rules = new Intl.PluralRules('pl-PL');
console.log(rules.select(0)); // "many"
console.log(rules.select(1)); // "one"
console.log(rules.select(2)); // "few"
console.log(rules.select(5)); // "many"
console.log(rules.select(22)); // "few"
console.log(rules.select(25)); // "many"
В арабском шесть категорий:
const rules = new Intl.PluralRules('ar-EG');
console.log(rules.select(0)); // "zero"
console.log(rules.select(1)); // "one"
console.log(rules.select(2)); // "two"
console.log(rules.select(3)); // "few"
console.log(rules.select(11)); // "many"
console.log(rules.select(100)); // "other"
Метод select() возвращает строку, определяющую категорию. Эту строку вы используете, чтобы выбрать подходящее сообщение для отображения.
Как сопоставлять категории множественного числа с сообщениями
После определения категории множественного числа нужно выбрать правильное сообщение для пользователя. Создайте объект, который сопоставляет каждую категорию с её сообщением, затем используйте строку категории для поиска сообщения.
const messages = {
one: '{count} file',
other: '{count} files'
};
function formatFileCount(count, locale) {
const rules = new Intl.PluralRules(locale);
const category = rules.select(count);
const message = messages[category];
return message.replace('{count}', count);
}
console.log(formatFileCount(1, 'en-US')); // "1 file"
console.log(formatFileCount(5, 'en-US')); // "5 files"
Этот подход работает для любого языка. Для польского вы указываете сообщения для всех трёх категорий, которые используются в языке:
const messages = {
one: '{count} plik',
few: '{count} pliki',
many: '{count} plików'
};
function formatFileCount(count, locale) {
const rules = new Intl.PluralRules(locale);
const category = rules.select(count);
const message = messages[category];
return message.replace('{count}', count);
}
console.log(formatFileCount(1, 'pl-PL')); // "1 plik"
console.log(formatFileCount(2, 'pl-PL')); // "2 pliki"
console.log(formatFileCount(5, 'pl-PL')); // "5 plików"
console.log(formatFileCount(22, 'pl-PL')); // "22 pliki"
Структура кода остаётся одинаковой для всех языков. Меняется только объект сообщений. Такое разделение позволяет переводчикам предоставлять правильные сообщения для своего языка без изменения кода.
Как обрабатывать отсутствующие категории множественного числа
В вашем объекте сообщений может не быть всех шести возможных категорий. В большинстве языков используется только две или три. Когда select() возвращает категорию, которой нет в вашем объекте сообщений, используйте категорию other по умолчанию.
function formatFileCount(count, locale, messages) {
const rules = new Intl.PluralRules(locale);
const category = rules.select(count);
const message = messages[category] || messages.other;
return message.replace('{count}', count);
}
const englishMessages = {
one: '{count} file',
other: '{count} files'
};
console.log(formatFileCount(1, 'en-US', englishMessages)); // "1 file"
console.log(formatFileCount(5, 'en-US', englishMessages)); // "5 files"
Этот подход гарантирует, что ваш код будет работать даже при неполном объекте сообщений. Категория other всегда есть в любом языке, поэтому её можно использовать как безопасную подстановку.
Как использовать правила множественного числа с порядковыми числительными
Конструктор Intl.PluralRules принимает опцию type, которая меняет способ определения категорий. По умолчанию тип "cardinal" используется для подсчёта предметов. Установите type: "ordinal", чтобы определять формы для порядковых числительных, таких как «1-й», «2-й», «3-й».
const cardinalRules = new Intl.PluralRules('en-US', { type: 'cardinal' });
console.log(cardinalRules.select(1)); // "one"
console.log(cardinalRules.select(2)); // "other"
console.log(cardinalRules.select(3)); // "other"
const ordinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
console.log(ordinalRules.select(1)); // "one"
console.log(ordinalRules.select(2)); // "two"
console.log(ordinalRules.select(3)); // "few"
console.log(ordinalRules.select(4)); // "other"
Кардинальные правила определяют формы типа «1 предмет, 2 предмета». Порядковые правила — формы типа «1-е место, 2-е место, 3-е место, 4-е место». Категории различаются, потому что грамматические шаблоны разные.
Как форматировать дробные значения
Метод select() работает с десятичными числами. В разных языках есть свои правила, как дробные значения относятся к категориям во множественном числе.
const rules = new Intl.PluralRules('en-US');
console.log(rules.select(1)); // "one"
console.log(rules.select(1.0)); // "one"
console.log(rules.select(1.5)); // "other"
console.log(rules.select(0.5)); // "other"
В английском языке 1.0 использует единственное число, а 1.5 — множественное. В некоторых языках для дробей есть отдельные правила, и они считаются отдельной категорией.
const messages = {
one: '{count} file',
other: '{count} files'
};
const rules = new Intl.PluralRules('en-US');
const count = 1.5;
const category = rules.select(count);
const message = messages[category];
console.log(message.replace('{count}', count)); // "1.5 files"
Передавайте десятичное число напрямую в select(). Метод автоматически применяет нужные языковые правила.
Как создать переиспользуемый форматтер для множественного числа
Вместо того чтобы повторять один и тот же шаблон по всему приложению, создайте функцию, которая инкапсулирует логику выбора формы по числу.
class PluralFormatter {
constructor(locale) {
this.locale = locale;
this.rules = new Intl.PluralRules(locale);
}
format(count, messages) {
const category = this.rules.select(count);
const message = messages[category] || messages.other;
return message.replace('{count}', count);
}
}
const formatter = new PluralFormatter('en-US');
const fileMessages = {
one: '{count} file',
other: '{count} files'
};
const itemMessages = {
one: '{count} item',
other: '{count} items'
};
console.log(formatter.format(1, fileMessages)); // "1 file"
console.log(formatter.format(5, fileMessages)); // "5 files"
console.log(formatter.format(1, itemMessages)); // "1 item"
console.log(formatter.format(3, itemMessages)); // "3 items"
Этот класс создаёт объект PluralRules один раз и переиспользует его для разных операций форматирования. Можно расширить его, чтобы добавить, например, форматирование числа с помощью Intl.NumberFormat перед вставкой в сообщение.
Как интегрировать правила множественного числа с системами перевода
Профессиональные системы перевода хранят сообщения с плейсхолдерами для разных форм множественного числа. При переводе вы указываете все формы, которые нужны для вашего языка.
const translations = {
'en-US': {
fileCount: {
one: '{count} file',
other: '{count} files'
},
downloadComplete: {
one: 'Download of {count} file complete',
other: 'Download of {count} files complete'
}
},
'pl-PL': {
fileCount: {
one: '{count} plik',
few: '{count} pliki',
many: '{count} plików'
},
downloadComplete: {
one: 'Pobieranie {count} pliku zakończone',
few: 'Pobieranie {count} plików zakończone',
many: 'Pobieranie {count} plików zakończone'
}
}
};
class Translator {
constructor(locale, translations) {
this.locale = locale;
this.translations = translations[locale] || {};
this.rules = new Intl.PluralRules(locale);
}
translate(key, count) {
const messages = this.translations[key];
if (!messages) return key;
const category = this.rules.select(count);
const message = messages[category] || messages.other;
return message.replace('{count}', count);
}
}
const translator = new Translator('en-US', translations);
console.log(translator.translate('fileCount', 1)); // "1 file"
console.log(translator.translate('fileCount', 5)); // "5 files"
console.log(translator.translate('downloadComplete', 1)); // "Download of 1 file complete"
console.log(translator.translate('downloadComplete', 5)); // "Download of 5 files complete"
const polishTranslator = new Translator('pl-PL', translations);
console.log(polishTranslator.translate('fileCount', 1)); // "1 plik"
console.log(polishTranslator.translate('fileCount', 2)); // "2 pliki"
console.log(polishTranslator.translate('fileCount', 5)); // "5 plików"
Такой подход отделяет переводимые данные от логики кода. Переводчики предоставляют сообщения для каждой категории, которая используется в языке. Ваш код применяет правила автоматически.
Как узнать, какие категории множественного числа используются в локали
Метод resolvedOptions() возвращает информацию об объекте PluralRules, но не показывает, какие категории используются в локали. Чтобы узнать все категории, протестируйте диапазон чисел и соберите уникальные возвращённые категории.
function getPluralCategories(locale) {
const rules = new Intl.PluralRules(locale);
const categories = new Set();
for (let i = 0; i <= 100; i++) {
categories.add(rules.select(i));
categories.add(rules.select(i + 0.5));
}
return Array.from(categories).sort();
}
console.log(getPluralCategories('en-US')); // ["one", "other"]
console.log(getPluralCategories('pl-PL')); // ["few", "many", "one"]
console.log(getPluralCategories('ar-EG')); // ["few", "many", "one", "other", "two", "zero"]
Этот способ проверяет целые и дробные значения в определённом диапазоне. Он определяет категории, которые нужно включить в объект сообщений для выбранной локали.