Тестирование множественных локалей

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

Тестирование множественных локалей позволяет выявить:

  • ошибки форматирования дат;
  • некорректное отображение валют;
  • проблемы с разделителями чисел;
  • ошибки pluralization;
  • несовместимость шаблонов сообщений;
  • отсутствие переводов;
  • ошибки парсинга;
  • проблемы RTL-интерфейсов;
  • некорректную работу календарей и таймзон.

Даже корректно работающий английский интерфейс может полностью ломаться при использовании арабской, японской или русской локали.


Подготовка тестовой среды

Установка зависимостей

npm install globalize cldr-data

Для тестирования удобно использовать:

npm install --save-dev jest

Подключение CLDR-данных

const Globalize = require("globalize");

Globalize.load(
  require("cldr-data/main/en/numbers.json"),
  require("cldr-data/main/ru/numbers.json"),
  require("cldr-data/main/de/numbers.json"),
  require("cldr-data/main/ar/numbers.json"),
  require("cldr-data/supplemental/likelySubtags.json"),
  require("cldr-data/supplemental/numberingSystems.json")
);

Стратегия тестирования локалей

Основные направления проверки

При тестировании интернационализации обычно проверяются:

Область Что тестируется
Числа Разделители, дробные части
Валюты Символы, позиции валют
Даты Форматы, календари
Время 12/24-часовой формат
Pluralization Правила множественного числа
Сообщения Переводы и шаблоны
RTL Отображение справа налево
Парсинг Корректность ввода

Тестирование форматирования чисел

Базовая проверка локалей

describe("Number formatting", () => {

  test("English locale", () => {
    const en = new Globalize("en");

    expect(en.formatNumber(12345.67))
      .toBe("12,345.67");
  });

  test("Russian locale", () => {
    const ru = new Globalize("ru");

    expect(ru.formatNumber(12345.67))
      .toBe("12 345,67");
  });

  test("German locale", () => {
    const de = new Globalize("de");

    expect(de.formatNumber(12345.67))
      .toBe("12.345,67");
  });

});

Проверка разделителей

Разные локали используют разные символы:

Локаль Тысячи Дробная часть
en , .
ru пробел ,
de . ,
fr пробел ,

Ошибки часто возникают при:

  • ручной обработке строк;
  • использовании parseFloat;
  • сериализации данных;
  • импорте CSV-файлов.

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

const locales = ["en", "ru", "de", "fr"];

describe("Multiple locale number tests", () => {

  locales.forEach(locale => {

    test(`Locale ${locale}`, () => {

      const g = new Globalize(locale);

      const result = g.formatNumber(1234567.89);

      expect(result).toBeTruthy();

    });

  });

});

Такой подход особенно полезен в CI/CD.


Тестирование валют

Проверка символов валют

describe("Currency formatting", () => {

  test("USD in English", () => {

    const en = new Globalize("en");

    expect(
      en.formatCurrency(1000, "USD")
    ).toContain("$");

  });

  test("RUB in Russian", () => {

    const ru = new Globalize("ru");

    expect(
      ru.formatCurrency(1000, "RUB")
    ).toContain("₽");

  });

});

Проверка расположения валюты

Формат валют зависит от локали.

Локаль Формат
en-US $1,000.00
fr-FR 1 000,00 €
ru-RU 1 000,00 ₽

Пример теста:

test("French currency format", () => {

  const fr = new Globalize("fr");

  const result = fr.formatCurrency(1000, "EUR");

  expect(result.endsWith("€")).toBe(true);

});

Тестирование дат и времени

Проверка локализованных дат

describe("Date formatting", () => {

  const date = new Date(2025, 0, 15);

  test("English date", () => {

    const en = new Globalize("en");

    expect(
      en.formatDate(date, { date: "short" })
    ).toMatch(/\d{1,2}\/\d{1,2}\/\d{2}/);

  });

  test("Russian date", () => {

    const ru = new Globalize("ru");

    const result = ru.formatDate(date, {
      datetime: "medium"
    });

    expect(result).toBeTruthy();

  });

});

Проблемы форматов дат

Наиболее частые ошибки:

Ошибка Причина
MM/DD вместо DD/MM Жёстко заданный формат
Потеря часового пояса UTC-конвертация
Неверный месяц Разница индексации
Ошибки AM/PM Локальная логика времени

Проверка 12/24-часового времени

test("Time formatting", () => {

  const en = new Globalize("en");
  const de = new Globalize("de");

  const date = new Date();

  const enTime = en.formatDate(date, {
    time: "short"
  });

  const deTime = de.formatDate(date, {
    time: "short"
  });

  expect(enTime).not.toEqual(deTime);

});

Тестирование pluralization

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

Разные языки используют разные формы числительных.

Например:

Язык Форм
English 2
Russian 3
Arabic 6

Тестирование русской локали

Globalize.loadMessages({
  ru: {
    item: {
      one: "товар",
      few: "товара",
      many: "товаров",
      other: "товара"
    }
  }
});

const ru = new Globalize("ru");

const plural = ru.pluralGenerator();

console.log(plural(1));
console.log(plural(2));
console.log(plural(5));

Автоматизированная проверка pluralization

describe("Plural rules", () => {

  test("Russian plural forms", () => {

    const ru = new Globalize("ru");

    const plural = ru.pluralGenerator();

    expect(plural(1)).toBe("one");
    expect(plural(2)).toBe("few");
    expect(plural(5)).toBe("many");

  });

});

Тестирование переводов

Проверка наличия переводов

Одна из наиболее распространённых проблем — отсутствие ключей.

Globalize.loadMessages({
  en: {
    hello: "Hello"
  }
});

Проверка:

test("Translation exists", () => {

  const en = new Globalize("en");

  expect(
    en.formatMessage("hello")
  ).toBe("Hello");

});

Проверка отсутствующих ключей

test("Missing translation", () => {

  const en = new Globalize("en");

  expect(() => {
    en.formatMessage("missing.key");
  }).toThrow();

});

Тестирование fallback-механизмов

Проверка резервной локали

Во многих приложениях используется fallback:

function getLocale(locale) {

  try {
    return new Globalize(locale);
  } catch {
    return new Globalize("en");
  }

}

Тест:

test("Fallback locale", () => {

  const locale = getLocale("unknown");

  expect(locale.locale.cldr.attributes.language)
    .toBe("en");

});

Тестирование RTL-локалей

Особенности RTL

RTL-языки:

  • арабский;
  • иврит;
  • персидский.

Основные проблемы:

  • неправильное выравнивание;
  • сломанный flex/grid;
  • инверсия иконок;
  • проблемы смешанного текста.

Проверка арабской локали

const ar = new Globalize("ar");

console.log(
  ar.formatNumber(123456)
);

Результат может использовать арабские цифры.


Проверка bidi-текста

Пример проблемной строки:

Цена: 100 USD

В RTL-интерфейсе порядок символов может нарушаться.

Для тестирования используются:

  • snapshot-тесты;
  • визуальные regression-тесты;
  • Playwright;
  • Cypress;
  • Percy.

Snapshot-тестирование

Проверка UI между локалями

Пример с React:

it("renders Russian locale", () => {

  const tree = renderer
    .create(<App locale="ru" />)
    .toJSON();

  expect(tree).toMatchSnapshot();

});

Преимущества snapshot-подхода

Snapshot-тесты помогают выявить:

  • переполнение строк;
  • изменение длины текста;
  • переносы;
  • проблемы RTL;
  • неожиданные изменения переводов.

Интеграционное тестирование

Проверка пользовательских сценариев

Тестируется полный сценарий:

  1. выбор локали;
  2. загрузка переводов;
  3. форматирование данных;
  4. отображение UI;
  5. переключение языка.

Пример теста переключения языка

test("Switch locale", () => {

  let g = new Globalize("en");

  expect(
    g.formatNumber(1000.5)
  ).toBe("1,000.5");

  g = new Globalize("de");

  expect(
    g.formatNumber(1000.5)
  ).toBe("1.000,5");

});

Тестирование загрузки CLDR

Проверка необходимых данных

Ошибка часто возникает при отсутствии нужных JSON-файлов.

expect(() => {

  Globalize.load(
    require("cldr-data/main/ru/numbers.json")
  );

}).not.toThrow();

Проверка incomplete locale

test("Missing CLDR data", () => {

  expect(() => {

    const g = new Globalize("ja");

    g.formatNumber(1000);

  }).toThrow();

});

Автоматическое тестирование всех локалей

Централизованный список локалей

const supportedLocales = [
  "en",
  "ru",
  "de",
  "fr",
  "ja",
  "ar"
];

Универсальный тест

describe("All locales", () => {

  supportedLocales.forEach(locale => {

    test(`${locale} works`, () => {

      const g = new Globalize(locale);

      expect(
        g.formatNumber(1234.56)
      ).toBeTruthy();

      expect(
        g.formatDate(new Date())
      ).toBeTruthy();

    });

  });

});

Тестирование производительности

Массовое форматирование

Некоторые локали работают медленнее из-за сложности правил.

test("Performance test", () => {

  const g = new Globalize("ar");

  const start = performance.now();

  for (let i = 0; i < 10000; i++) {
    g.formatNumber(i);
  }

  const end = performance.now();

  expect(end - start).toBeLessThan(500);

});

Тестирование парсинга

Проверка parseNumber

test("Parse localized number", () => {

  const de = new Globalize("de");

  const result = de.parseNumber("1.234,56");

  expect(result).toBe(1234.56);

});

Частые ошибки парсинга

Ошибка Причина
NaN Неверный разделитель
Потеря дробной части parseInt
Неверное число Смешение локалей

Проверка edge-case сценариев

Большие числа

test("Large number", () => {

  const en = new Globalize("en");

  const result = en.formatNumber(999999999999);

  expect(result).toBeTruthy();

});

Отрицательные значения

test("Negative currency", () => {

  const en = new Globalize("en");

  const result = en.formatCurrency(-500, "USD");

  expect(result).toContain("-");

});

Нулевые значения

test("Zero plural", () => {

  const ru = new Globalize("ru");

  const plural = ru.pluralGenerator();

  expect(plural(0)).toBe("many");

});

CI/CD и тестирование локалей

Запуск тестов в pipeline

Пример GitHub Actions:

name: i18n-tests

on: [push]

jobs:

  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: 20

      - run: npm install
      - run: npm test

Подходы к организации тестов

Разделение по локалям

tests/
  en/
  ru/
  de/

Разделение по функциональности

tests/
  numbers/
  dates/
  currency/
  plural/

Лучшие практики

Использование фиксированных дат

Плавающие даты делают тесты нестабильными.

Правильно:

new Date(2025, 0, 1)

Неправильно:

new Date()

Изоляция локалей

Каждый тест должен явно задавать локаль.

const g = new Globalize("ru");

Не рекомендуется использовать глобальное состояние.


Проверка реальных языков

Тестирование только en недостаточно.

Минимальный набор:

  • en;
  • ru;
  • de;
  • fr;
  • ar;
  • ja.

Такой набор покрывает:

  • латиницу;
  • RTL;
  • сложные plural rules;
  • разные системы чисел;
  • различные форматы дат.

Типичные ошибки при тестировании

Жёстко заданные строки

Плохо:

expect(result).toBe("1,000.00");

Лучше:

expect(result).toContain("1");

Игнорирование пробелов

Некоторые локали используют:

  • non-breaking space;
  • narrow space;
  • Unicode-разделители.

Из-за этого обычное сравнение строк может ломаться.


Отсутствие RTL-тестов

RTL часто проверяется только перед релизом, что приводит к критическим UI-багам.


Проверка только браузера разработчика

Локали могут вести себя по-разному:

  • в Node.js;
  • в Chrome;
  • в Safari;
  • в мобильных браузерах.

Комплексный пример тестирования

describe("Localization suite", () => {

  const locales = ["en", "ru", "de", "ar"];

  locales.forEach(locale => {

    describe(locale, () => {

      const g = new Globalize(locale);

      test("number formatting", () => {
        expect(
          g.formatNumber(1234.56)
        ).toBeTruthy();
      });

      test("currency formatting", () => {
        expect(
          g.formatCurrency(100, "USD")
        ).toBeTruthy();
      });

      test("date formatting", () => {
        expect(
          g.formatDate(new Date(2025, 0, 1))
        ).toBeTruthy();
      });

      test("plural rules", () => {

        const plural = g.pluralGenerator();

        expect(
          plural(5)
        ).toBeTruthy();

      });

    });

  });

});