Настройка множественных форм

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


Проблема множественных форм

В английском языке две формы: 1 minute и 2 minutes. В русском — три:

1 минуту    (одиночная: оканчивается на 1, кроме 11)
2 минуты    (дробная: оканчивается на 2–4, кроме 12–14)
5 минут     (множественная: всё остальное)

Без учёта этих правил будут появляться ошибки: “2 минут назад” вместо “2 минуты назад”.


Алгоритм выбора формы для русского

function pluralRu(number, one, few, many) {
  const n = Math.abs(number) % 100;
  const n1 = n % 10;

  if (n >= 11 && n <= 19) return many;
  if (n1 === 1) return one;
  if (n1 >= 2 && n1 <= 4) return few;
  return many;
}

Примеры:

pluralRu(1,  'минуту', 'минуты', 'минут') // → "минуту"
pluralRu(2,  'минуту', 'минуты', 'минут') // → "минуты"
pluralRu(5,  'минуту', 'минуты', 'минут') // → "минут"
pluralRu(11, 'минуту', 'минуты', 'минут') // → "минут"
pluralRu(21, 'минуту', 'минуты', 'минут') // → "минуту"
pluralRu(22, 'минуту', 'минуты', 'минут') // → "минуты"

Применение в функции локали

import { register } from 'timeago.js';

function pluralRu(n, one, few, many) {
  const mod100 = n % 100;
  const mod10  = mod100 % 10;
  if (mod100 >= 11 && mod100 <= 14) return many;
  if (mod10 === 1) return one;
  if (mod10 >= 2 && mod10 <= 4) return few;
  return many;
}

register('ru_plural', (number, index) => {
  const pairs = [
    ['только что', 'прямо сейчас'],
    [
      `${number} ${pluralRu(number, 'секунду', 'секунды', 'секунд')} назад`,
      `через ${number} ${pluralRu(number, 'секунду', 'секунды', 'секунд')}`,
    ],
    [
      `${number} ${pluralRu(number, 'секунду', 'секунды', 'секунд')} назад`,
      `через ${number} ${pluralRu(number, 'секунду', 'секунды', 'секунд')}`,
    ],
    ['1 минуту назад', 'через 1 минуту'],
    [
      `${number} ${pluralRu(number, 'минуту', 'минуты', 'минут')} назад`,
      `через ${number} ${pluralRu(number, 'минуту', 'минуты', 'минут')}`,
    ],
    ['1 час назад', 'через 1 час'],
    [
      `${number} ${pluralRu(number, 'час', 'часа', 'часов')} назад`,
      `через ${number} ${pluralRu(number, 'час', 'часа', 'часов')}`,
    ],
    ['1 день назад', 'через 1 день'],
    [
      `${number} ${pluralRu(number, 'день', 'дня', 'дней')} назад`,
      `через ${number} ${pluralRu(number, 'день', 'дня', 'дней')}`,
    ],
    ['1 неделю назад', 'через 1 неделю'],
    [
      `${number} ${pluralRu(number, 'неделю', 'недели', 'недель')} назад`,
      `через ${number} ${pluralRu(number, 'неделю', 'недели', 'недель')}`,
    ],
    ['1 месяц назад', 'через 1 месяц'],
    [
      `${number} ${pluralRu(number, 'месяц', 'месяца', 'месяцев')} назад`,
      `через ${number} ${pluralRu(number, 'месяц', 'месяца', 'месяцев')}`,
    ],
    ['1 год назад', 'через 1 год'],
    [
      `${number} ${pluralRu(number, 'год', 'года', 'лет')} назад`,
      `через ${number} ${pluralRu(number, 'год', 'года', 'лет')}`,
    ],
  ];
  return pairs[index];
});

Использование Intl.PluralRules (современный подход)

const pluralRules = new Intl.PluralRules('ru');

function getPluralForm(number, forms) {
  const category = pluralRules.select(number); // 'one', 'few', 'many', 'other'
  return forms[category] || forms.other;
}

const minuteForms = {
  one:   'минуту',
  few:   'минуты',
  many:  'минут',
  other: 'минуты',
};

getPluralForm(1,  minuteForms); // → "минуту"
getPluralForm(3,  minuteForms); // → "минуты"
getPluralForm(10, minuteForms); // → "минут"

Польский язык (четыре формы)

Польский требует четырёх форм:

function pluralPl(n, one, few, many, other) {
  if (n === 1) return one;
  const mod100 = n % 100;
  const mod10  = n % 10;
  if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) return few;
  if (mod10 === 1 && mod100 !== 11) return one;
  return mod10 >= 5 ? many : other;
}

Арабский язык (шесть форм)

const pluralAr = new Intl.PluralRules('ar');

function getArabicForm(n, forms) {
  return forms[pluralAr.select(n)] || forms.other;
}

Арабский использует формы: zero, one, two, few, many, other.


Универсальная обёртка через Intl.PluralRules

function makePlural(locale, forms) {
  const rules = new Intl.PluralRules(locale);
  return (n) => forms[rules.select(n)] || forms.other;
}

const ruMinute = makePlural('ru', {
  one:   'минуту',
  few:   'минуты',
  many:  'минут',
  other: 'минуты',
});

ruMinute(1);  // → "минуту"
ruMinute(2);  // → "минуты"
ruMinute(11); // → "минут"

Тестирование множественных форм

const testCases = [1, 2, 3, 4, 5, 10, 11, 12, 20, 21, 22, 100, 101];

testCases.forEach(n => {
  console.log(`${n}: ${pluralRu(n, 'минуту', 'минуты', 'минут')}`);
});

Ожидаемый вывод:

1: минуту
2: минуты
3: минуты
4: минуты
5: минут
10: минут
11: минут
12: минут
20: минут
21: минуту
22: минуты
100: минут
101: минуту

Особенности глаголов в русском

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

1 минуту назад  (вин. падеж)
2 минуты назад  (род. падеж)
5 минут назад   (род. мн. ч.)

через 1 минуту  (вин. падеж)
через 2 минуты  (род. падеж)
через 5 минут   (род. мн. ч.)

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