Получите все формы множественного числа, доступные в локали

Узнайте, для каких категорий множественного числа нужно предоставить переводы

Введение

При создании многоязычных приложений необходимо предоставлять разные формы текста для разных количеств. На английском языке вы пишете "1 item" и "2 items". Это кажется простым, пока вы не начнете поддерживать другие языки.

В русском языке используются три разные формы в зависимости от количества. В арабском языке их шесть. Некоторые языки используют одну и ту же форму для всех количеств. Прежде чем вы сможете предоставить переводы для этих форм, вам нужно знать, какие формы существуют в каждом языке.

JavaScript предоставляет способ узнать, какие категории множественного числа используются в локали. Метод resolvedOptions() экземпляра PluralRules возвращает свойство pluralCategories, которое перечисляет все формы множественного числа, необходимые для локали. Это точно указывает, какие переводы нужно предоставить, без догадок или ведения таблиц правил для каждого языка.

Что такое категории множественного числа

Категории множественного числа — это стандартизированные названия для различных форм множественного числа, используемых в разных языках. Unicode CLDR (Общий репозиторий языковых данных) определяет шесть категорий: zero, one, two, few, many и other.

Не каждый язык использует все шесть категорий. Английский язык использует только две: one и other. Категория one применяется к количеству 1, а other — ко всему остальному.

Арабский язык использует все шесть категорий. Категория zero применяется к 0, one — к 1, two — к 2, few — к количествам от 3 до 10, many — к количествам от 11 до 99, а other — к количествам от 100 и выше.

Русский язык использует три категории: one для количеств, оканчивающихся на 1 (кроме 11), few для количеств, оканчивающихся на 2-4 (кроме 12-14), и many для всего остального.

Японский и китайский языки используют только категорию other, так как в этих языках нет различий между единственным и множественным числом.

Эти категории представляют собой лингвистические правила каждого языка. Когда вы предоставляете переводы, вы создаете одну строку для каждой категории, используемой в языке.

Получение категорий множественного числа с помощью resolvedOptions

Метод resolvedOptions() экземпляра PluralRules возвращает объект, содержащий информацию о правилах, включая категории множественного числа, используемые в данном языке.

const enRules = new Intl.PluralRules('en-US');
const options = enRules.resolvedOptions();

console.log(options.pluralCategories);
// Вывод: ["one", "other"]

Свойство pluralCategories — это массив строк. Каждая строка представляет одну из шести стандартных категорий. Массив содержит только те категории, которые действительно используются в данном языке.

Для английского языка массив содержит one и other, так как английский различает единственное и множественное число.

Для языка с более сложными правилами массив содержит больше категорий:

const arRules = new Intl.PluralRules('ar-EG');
const options = arRules.resolvedOptions();

console.log(options.pluralCategories);
// Вывод: ["zero", "one", "two", "few", "many", "other"]

Арабский язык использует все шесть категорий, поэтому массив содержит все шесть значений.

Просмотр категорий множественного числа для разных локалей

Разные языки имеют разные правила множественного числа, что означает использование различных наборов категорий. Сравните несколько языков, чтобы увидеть различия:

const locales = ['en-US', 'ar-EG', 'ru-RU', 'pl-PL', 'ja-JP', 'zh-CN'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  console.log(`${locale}: [${categories.join(', ')}]`);
});

// Вывод:
// en-US: [one, other]
// ar-EG: [zero, one, two, few, many, other]
// ru-RU: [one, few, many, other]
// pl-PL: [one, few, many, other]
// ja-JP: [other]
// zh-CN: [other]

Английский язык имеет две категории. Арабский — шесть. Русский и польский — по четыре. Японский и китайский имеют только одну, так как они вообще не различают формы множественного числа.

Эти различия показывают, почему нельзя предполагать, что все языки работают так же, как английский. Необходимо проверять, какие категории используются в каждой локали, и предоставлять соответствующие переводы для каждой из них.

Понимание значений категорий для каждого языка

Одно и то же название категории может означать разные вещи в разных языках. Категория one в английском языке применяется только к числу 1. В русском языке категория one применяется к числам, оканчивающимся на 1, за исключением 11, то есть включает 1, 21, 31, 101 и так далее.

Проверьте, какие числа соответствуют каким категориям в разных языках:

const enRules = new Intl.PluralRules('en-US');
const ruRules = new Intl.PluralRules('ru-RU');

const numbers = [0, 1, 2, 3, 5, 11, 21, 22, 100];

console.log('Английский:');
numbers.forEach(n => {
  console.log(`  ${n}: ${enRules.select(n)}`);
});

console.log('Русский:');
numbers.forEach(n => {
  console.log(`  ${n}: ${ruRules.select(n)}`);
});

// Результат:
// Английский:
//   0: other
//   1: one
//   2: other
//   3: other
//   5: other
//   11: other
//   21: other
//   22: other
//   100: other
// Русский:
//   0: many
//   1: one
//   2: few
//   3: few
//   5: many
//   11: many
//   21: one
//   22: few
//   100: many

В английском языке только число 1 относится к категории one. В русском языке числа 1 и 21 относятся к категории one, так как они оканчиваются на 1. Числа 2, 3 и 22 относятся к категории few, так как они оканчиваются на 2-4. Числа 0, 5, 11 и 100 относятся к категории many.

Это демонстрирует, что невозможно предсказать, какая категория применяется к числу, не зная правил языка. Массив pluralCategories показывает, какие категории существуют, а метод select() определяет, какая категория применяется к каждому числу.

Получение категорий для порядковых чисел

Порядковые числа, такие как 1-й, 2-й, 3-й, имеют свои собственные правила множественного числа, отличные от количественных чисел. Создайте экземпляр PluralRules с параметром type: 'ordinal', чтобы получить категории для порядковых чисел:

const enCardinalRules = new Intl.PluralRules('en-US', { type: 'cardinal' });
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

console.log('Количественные:', enCardinalRules.resolvedOptions().pluralCategories);
// Результат: Количественные: ["one", "other"]

console.log('Порядковые:', enOrdinalRules.resolvedOptions().pluralCategories);
// Результат: Порядковые: ["one", "two", "few", "other"]

Английские количественные числа используют две категории. Английские порядковые числа используют четыре категории, так как порядковые числа должны различать 1-й, 2-й, 3-й и все остальные.

Категории порядковых чисел соответствуют порядковым суффиксам:

const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

const numbers = [1, 2, 3, 4, 11, 21, 22, 23];

numbers.forEach(n => {
  const category = enOrdinalRules.select(n);
  console.log(`${n}: ${category}`);
});

// Результат:
// 1: one
// 2: two
// 3: few
// 4: other
// 11: other
// 21: one
// 22: two
// 23: few

Категория one соответствует суффиксу -й (1-й, 21-й), two — суффиксу -й (2-й, 22-й), few — суффиксу -й (3-й, 23-й), а other — суффиксу -й (4-й, 11-й).

Разные языки имеют разные категории порядковых чисел:

const locales = ['en-US', 'es-ES', 'fr-FR'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
  const categories = rules.resolvedOptions().pluralCategories;
  console.log(`${locale}: [${categories.join(', ')}]`);
});

// Результат:
// en-US: [one, two, few, other]
// es-ES: [other]
// fr-FR: [one, other]

Испанский язык использует только одну категорию порядковых чисел, так как порядковые числа в испанском языке следуют более простой схеме. Французский язык использует две категории, чтобы различать первое место от всех остальных.

Использование категорий множественного числа для создания карт перевода

Когда вы знаете, какие категории используются в локали, вы можете создать карту перевода с точным количеством записей:

function buildTranslationMap(locale, translations) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;

  const map = new Map();

  categories.forEach(category => {
    if (translations[category]) {
      map.set(category, translations[category]);
    } else {
      console.warn(`Отсутствует перевод для категории "${category}" в локали "${locale}"`);
    }
  });

  return map;
}

const enTranslations = {
  one: 'item',
  other: 'items'
};

const arTranslations = {
  zero: 'لا توجد عناصر',
  one: 'عنصر واحد',
  two: 'عنصران',
  few: 'عناصر',
  many: 'عنصرًا',
  other: 'عنصر'
};

const enMap = buildTranslationMap('en-US', enTranslations);
const arMap = buildTranslationMap('ar-EG', arTranslations);

console.log(enMap);
// Output: Map(2) { 'one' => 'item', 'other' => 'items' }

console.log(arMap);
// Output: Map(6) { 'zero' => 'لا توجد عناصر', 'one' => 'عنصر واحد', ... }

Эта функция проверяет, что вы предоставили переводы для всех необходимых категорий, и предупреждает, если какие-либо из них отсутствуют. Это предотвращает ошибки во время выполнения, когда используется категория, но для нее нет перевода.

Проверка полноты перевода

Используйте категории множественного числа, чтобы убедиться, что ваши переводы включают все необходимые формы, перед развертыванием в продакшн:

function validateTranslations(locale, translations) {
  const rules = new Intl.PluralRules(locale);
  const requiredCategories = rules.resolvedOptions().pluralCategories;
  const providedCategories = Object.keys(translations);

  const missing = requiredCategories.filter(cat => !providedCategories.includes(cat));
  const extra = providedCategories.filter(cat => !requiredCategories.includes(cat));

  if (missing.length > 0) {
    console.error(`Локаль ${locale} не содержит категорий: ${missing.join(', ')}`);
    return false;
  }

  if (extra.length > 0) {
    console.warn(`Локаль ${locale} содержит неиспользуемые категории: ${extra.join(', ')}`);
  }

  return true;
}

const enTranslations = {
  one: 'item',
  other: 'items'
};

const incompleteArTranslations = {
  one: 'عنصر واحد',
  other: 'عنصر'
};

validateTranslations('en-US', enTranslations);
// Output: true

validateTranslations('ar-EG', incompleteArTranslations);
// Output: Локаль ar-EG не содержит категорий: zero, two, few, many
// Output: false

Эта проверка выявляет отсутствующие переводы на этапе разработки, а не тогда, когда пользователи сталкиваются с непереведенным текстом.

Создание динамических интерфейсов перевода

При создании инструментов для переводчиков запросите категории множественного числа, чтобы показать, какие именно формы требуют перевода:

function generateTranslationForm(locale, key) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;

  const form = document.createElement('div');
  form.className = 'translation-form';

  const heading = document.createElement('h3');
  heading.textContent = `Переведите "${key}" для ${locale}`;
  form.appendChild(heading);

  categories.forEach(category => {
    const label = document.createElement('label');
    label.textContent = `${category}:`;

    const input = document.createElement('input');
    input.type = 'text';
    input.name = `${key}.${category}`;
    input.placeholder = `Введите форму для ${category}`;

    const wrapper = document.createElement('div');
    wrapper.appendChild(label);
    wrapper.appendChild(input);
    form.appendChild(wrapper);
  });

  return form;
}

const enForm = generateTranslationForm('en-US', 'items');
const arForm = generateTranslationForm('ar-EG', 'items');

document.body.appendChild(enForm);
document.body.appendChild(arForm);

Этот код создает форму с правильным количеством полей ввода для каждого языка. Для английского создаются два поля (one и other), а для арабского — шесть полей (zero, one, two, few, many и other).

Сравнение категорий между языками

При управлении переводами для нескольких языков сравните, какие категории они используют, чтобы понять сложность перевода:

function compareLocalePluralCategories(locales) {
  const comparison = {};

  locales.forEach(locale => {
    const rules = new Intl.PluralRules(locale);
    const categories = rules.resolvedOptions().pluralCategories;
    comparison[locale] = categories;
  });

  return comparison;
}

const locales = ['en-US', 'es-ES', 'ar-EG', 'ru-RU', 'ja-JP'];
const comparison = compareLocalePluralCategories(locales);

console.log(comparison);
// Вывод:
// {
//   'en-US': ['one', 'other'],
//   'es-ES': ['one', 'other'],
//   'ar-EG': ['zero', 'one', 'two', 'few', 'many', 'other'],
//   'ru-RU': ['one', 'few', 'many', 'other'],
//   'ja-JP': ['other']
// }

Это показывает, что английский и испанский используют одинаковые категории множественного числа, что упрощает повторное использование структур перевода между ними. Арабский требует значительно больше работы, так как использует шесть категорий.

Проверка использования конкретной категории в локали

Прежде чем использовать конкретную категорию множественного числа в вашем коде, проверьте, действительно ли локаль её использует:

function localeUsesCategory(locale, category) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  return categories.includes(category);
}

console.log(localeUsesCategory('en-US', 'zero'));
// Вывод: false

console.log(localeUsesCategory('ar-EG', 'zero'));
// Вывод: true

console.log(localeUsesCategory('ja-JP', 'one'));
// Вывод: false

Это позволяет избежать предположений, что каждая локаль имеет категорию zero или one. Используйте эту проверку, чтобы безопасно реализовать логику, зависящую от категории.

Понимание категории other

Каждый язык использует категорию other. Эта категория служит как случай по умолчанию, когда никакая другая категория не подходит.

В английском языке категория other охватывает все числа, кроме 1. В арабском языке категория other охватывает большие числа, такие как 100 и выше. В японском языке категория other охватывает все числа, так как японский язык не различает формы множественного числа.

Всегда предоставляйте перевод для категории other. Эта категория гарантированно существует в каждой локали и будет использоваться, если ни одна более конкретная категория не подходит.

const locales = ['en-US', 'ar-EG', 'ru-RU', 'ja-JP'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  const hasOther = categories.includes('other');
  console.log(`${locale} использует "other": ${hasOther}`);
});

// Вывод:
// en-US использует "other": true
// ar-EG использует "other": true
// ru-RU использует "other": true
// ja-JP использует "other": true

Получение всех разрешённых опций вместе

Метод resolvedOptions() возвращает не только категории множественного числа. Он включает информацию о локали, типе и параметрах форматирования чисел:

const rules = new Intl.PluralRules('de-DE', {
  type: 'cardinal',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

const options = rules.resolvedOptions();

console.log(options);
// Вывод:
// {
//   locale: 'de-DE',
//   type: 'cardinal',
//   pluralCategories: ['one', 'other'],
//   minimumIntegerDigits: 1,
//   minimumFractionDigits: 2,
//   maximumFractionDigits: 2,
//   minimumSignificantDigits: undefined,
//   maximumSignificantDigits: undefined
// }

Свойство pluralCategories — это одна из частей информации в объекте разрешённых опций. Другие свойства сообщают точную конфигурацию, которую использует экземпляр PluralRules, включая любые параметры, установленные по умолчанию.

Кэширование категорий множественного числа для повышения производительности

Создание экземпляров PluralRules и вызов метода resolvedOptions() имеют свою стоимость. Кэшируйте результаты для каждого локаля вместо повторного их запроса:

const categoriesCache = new Map();

function getPluralCategories(locale, type = 'cardinal') {
  const key = `${locale}:${type}`;

  if (categoriesCache.has(key)) {
    return categoriesCache.get(key);
  }

  const rules = new Intl.PluralRules(locale, { type });
  const categories = rules.resolvedOptions().pluralCategories;

  categoriesCache.set(key, categories);

  return categories;
}

const enCardinal = getPluralCategories('en-US', 'cardinal');
const enOrdinal = getPluralCategories('en-US', 'ordinal');
const arCardinal = getPluralCategories('ar-EG', 'cardinal');

console.log('en-US cardinal:', enCardinal);
console.log('en-US ordinal:', enOrdinal);
console.log('ar-EG cardinal:', arCardinal);

// Последующие вызовы используют кэшированные результаты
const enCardinal2 = getPluralCategories('en-US', 'cardinal');
// Новый экземпляр PluralRules не создается

Этот подход особенно важен в приложениях, которые форматируют множество строк с множественным числом или поддерживают множество локалей.

Поддержка браузеров и совместимость

Свойство pluralCategories в методе resolvedOptions() было добавлено в JavaScript в 2020 году. Оно поддерживается в Chrome 106+, Firefox 116+, Safari 15.4+ и Edge 106+.

Старые браузеры, которые поддерживают Intl.PluralRules, но не поддерживают pluralCategories, будут возвращать undefined для этого свойства. Проверьте его наличие перед использованием:

function getPluralCategories(locale) {
  const rules = new Intl.PluralRules(locale);
  const options = rules.resolvedOptions();

  if (options.pluralCategories) {
    return options.pluralCategories;
  }

  // Резервный вариант для старых браузеров
  return ['one', 'other'];
}

Этот резервный вариант предполагает простую систему из двух категорий, которая работает для английского и многих европейских языков, но может быть некорректной для языков с более сложными правилами. Для лучшей совместимости предоставляйте резервные варианты, специфичные для языка, или используйте полифил.