При интернационализации приложений недостаточно проверить работу только одной локали. Библиотека Globalize активно использует данные CLDR, а значит поведение форматирования зависит от языка, региона, календаря, системы счисления, правил плюрализации и локальных стандартов записи чисел, дат и валют.
Тестирование множественных локалей позволяет выявить:
Даже корректно работающий английский интерфейс может полностью ломаться при использовании арабской, японской или русской локали.
npm install globalize cldr-data
Для тестирования удобно использовать:
npm install --save-dev jest
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 | пробел | , |
Ошибки часто возникают при:
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 | Локальная логика времени |
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);
});
Разные языки используют разные формы числительных.
Например:
| Язык | Форм |
|---|---|
| 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));
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:
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-языки:
Основные проблемы:
const ar = new Globalize("ar");
console.log(
ar.formatNumber(123456)
);
Результат может использовать арабские цифры.
Пример проблемной строки:
Цена: 100 USD
В RTL-интерфейсе порядок символов может нарушаться.
Для тестирования используются:
Пример с React:
it("renders Russian locale", () => {
const tree = renderer
.create(<App locale="ru" />)
.toJSON();
expect(tree).toMatchSnapshot();
});
Snapshot-тесты помогают выявить:
Тестируется полный сценарий:
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");
});
Ошибка часто возникает при отсутствии нужных JSON-файлов.
expect(() => {
Globalize.load(
require("cldr-data/main/ru/numbers.json")
);
}).not.toThrow();
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);
});
test("Parse localized number", () => {
const de = new Globalize("de");
const result = de.parseNumber("1.234,56");
expect(result).toBe(1234.56);
});
| Ошибка | Причина |
|---|---|
| NaN | Неверный разделитель |
| Потеря дробной части | parseInt |
| Неверное число | Смешение локалей |
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");
});
Пример 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 недостаточно.
Минимальный набор:
Такой набор покрывает:
Плохо:
expect(result).toBe("1,000.00");
Лучше:
expect(result).toContain("1");
Некоторые локали используют:
Из-за этого обычное сравнение строк может ломаться.
RTL часто проверяется только перед релизом, что приводит к критическим UI-багам.
Локали могут вести себя по-разному:
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();
});
});
});
});