Как выбрать правильную форму множественного числа для разных языков?
Используйте 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} файл',
other: '{count} файлов'
};
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 файл"
console.log(formatFileCount(5, 'en-US')); // "5 файлов"
Этот шаблон работает для любого языка. Для польского языка вы предоставляете сообщения для всех трёх категорий, которые используются в языке:
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} файл',
other: '{count} файлов'
};
console.log(formatFileCount(1, 'en-US', englishMessages)); // "1 файл"
console.log(formatFileCount(5, 'en-US', englishMessages)); // "5 файлов"
Этот шаблон гарантирует, что ваш код будет работать, даже если объект сообщений неполный. Категория 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} файл',
other: '{count} файлов'
};
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 файлов"
Передайте десятичное число напрямую в 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} файл',
other: '{count} файлов'
};
const itemMessages = {
one: '{count} предмет',
other: '{count} предметов'
};
console.log(formatter.format(1, fileMessages)); // "1 файл"
console.log(formatter.format(5, fileMessages)); // "5 файлов"
console.log(formatter.format(1, itemMessages)); // "1 предмет"
console.log(formatter.format(3, itemMessages)); // "3 предмета"
Этот класс создает объект 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"]
Этот метод тестирует целые числа и дробные значения в диапазоне. Он фиксирует категории, которые ваш объект сообщений должен включать для данной локали.