Тестирование локализации

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


Структура LocaleFunc

Прежде чем тестировать, нужно знать, что именно проверять. Функция локали принимает (number, index) и возвращает [прошлое, будущее]. Индекс соответствует интервалу:

Index Интервал
0 < 45 секунд
1 1 секунда
2 2-44 секунды
3 45-89 секунд
4 1-2 минуты
5 2-44 минуты
6 45-89 минут
7 1-2 часа
8 2-21 часа
9 22-35 часов
10 1-2 дня
11 2-25 дней
12 26-345 дней
13 1-2 года
14 > 2 лет

Базовый тест LocaleFunc

import { customRuLocale } from '../src/locales/ru';

describe('customRuLocale', () => {
  it('возвращает кортеж [string, string] для каждого индекса', () => {
    for (let index = 0; index <= 14; index++) {
      const result = customRuLocale(1, index);
      expect(result).toHaveLength(2);
      expect(typeof result[0]).toBe('string');
      expect(typeof result[1]).toBe('string');
    }
  });

  it('не возвращает пустые строки', () => {
    for (let index = 0; index <= 14; index++) {
      const [past, future] = customRuLocale(1, index);
      expect(past.length).toBeGreaterThan(0);
      expect(future.length).toBeGreaterThan(0);
    }
  });
});

Тест конкретных форм

import { ruLocale } from '../src/locales/ru';

describe('ruLocale — конкретные формы', () => {
  it('индекс 0: "только что"', () => {
    expect(ruLocale(1, 0)[0]).toBe('только что');
  });

  it('индекс 4: "минуту назад"', () => {
    expect(ruLocale(1, 4)[0]).toBe('минуту назад');
  });

  it('индекс 5: "5 минут назад" (число подставляется)', () => {
    expect(ruLocale(5, 5)[0]).toBe('5 минут назад');
  });

  it('индекс 7: "час назад"', () => {
    expect(ruLocale(1, 7)[0]).toBe('час назад');
  });

  it('индекс 13: "год назад"', () => {
    expect(ruLocale(1, 13)[0]).toBe('год назад');
  });
});

Тест русского склонения

Русский язык имеет три формы множественного числа. Важно проверить пограничные значения:

import { format } from 'timeago.js';

const NOW = new Date('2025-06-01T12:00:00Z').getTime();

beforeEach(() => {
  jest.useFakeTimers();
  jest.setSystemTime(NOW);
});

afterEach(() => jest.useRealTimers());

const cases: Array<[number, RegExp]> = [
  [1 * 60_000,           /минуту назад/],       // 1 минута
  [2 * 60_000,           /2 минуты назад/],      // 2 минуты
  [5 * 60_000,           /5 минут назад/],       // 5 минут
  [11 * 60_000,          /11 минут назад/],      // 11 минут
  [21 * 60_000,          /21 минуту назад/],     // 21 минута
  [1 * 3600_000,         /час назад/],           // 1 час
  [2 * 3600_000,         /2 часа назад/],        // 2 часа
  [5 * 3600_000,         /5 часов назад/],       // 5 часов
  [1 * 86400_000,        /день назад/],          // 1 день
  [2 * 86400_000,        /2 дня назад/],         // 2 дня
  [5 * 86400_000,        /5 дней назад/],        // 5 дней
];

it.each(cases)('%i мс назад соответствует %s', (ms, pattern) => {
  expect(format(new Date(NOW - ms), 'ru')).toMatch(pattern);
});

Тест будущего времени

it('форматирует будущее время', () => {
  const future = new Date(NOW + 3600_000); // через час
  const result = format(future, 'ru');
  expect(result).toMatch(/через|позже/);
});

Тест с relativeDate (кастомная точка отсчёта)

import { format } from 'timeago.js';

it('поддерживает кастомную точку отсчёта', () => {
  const base     = new Date('2025-06-01T12:00:00Z');
  const target   = new Date('2025-06-01T11:00:00Z');

  const result = format(target, 'ru', { relativeDate: base });
  expect(result).toMatch(/час/);
});

Тест нескольких локалей

const LOCALES: Array<[string, string, RegExp]> = [
  ['ru',    '2025-05-31T12:00:00Z', /день/],
  ['en_US', '2025-05-31T12:00:00Z', /day/],
  ['de',    '2025-05-31T12:00:00Z', /Tag/],
  ['fr',    '2025-05-31T12:00:00Z', /jour/],
  ['ja',    '2025-05-31T12:00:00Z', /日/],
  ['zh_CN', '2025-05-31T12:00:00Z', /天/],
];

describe.each(LOCALES)('locale "%s"', (locale, date, pattern) => {
  it(`форматирует "1 день назад" на ${locale}`, () => {
    jest.useFakeTimers();
    jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());
    expect(format(date, locale)).toMatch(pattern);
    jest.useRealTimers();
  });
});

Тест регистрации и работы кастомной локали

import { register, format } from 'timeago.js';

const TEST_LOCALE = 'test_custom_' + Date.now();

const forms: [string, string][] = new Array(15).fill(null).map((_, i) =>
  [`PAST_${i}`, `FUTURE_${i}`]
);

beforeAll(() => {
  register(TEST_LOCALE, (n, i) => forms[i] ?? ['PAST_DEFAULT', 'FUTURE_DEFAULT']);
});

it('использует форму из кастомной локали для index 0', () => {
  jest.useFakeTimers();
  jest.setSystemTime(Date.now());
  const result = format(new Date(), TEST_LOCALE);
  expect(result).toBe('PAST_0');
  jest.useRealTimers();
});

Тест RTL локали (арабский)

it('форматирует арабскую локаль', () => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2025-06-01T12:00:00Z').getTime());

  const result = format('2025-06-01T11:00:00Z', 'ar');
  // Арабский использует правосторонние знаки
  expect(typeof result).toBe('string');
  expect(result.length).toBeGreaterThan(0);

  jest.useRealTimers();
});

Матрица тестирования локали

Аспект Что тестировать
Структура LocaleFunc Все индексы 0-14, кортеж из 2 строк
Пустые строки Ни один элемент не пустой
Подстановка числа %s заменяется на реальное число
Прошлое и будущее Обе ветки работают
Граничные значения 1, 2, 5, 11, 21 для склонений
Пересечение с format Кастомная локаль корректно применяется