Форматирование дат, чисел, валют и сообщений относится к критически важным частям интернационализации. Ошибки в локализованных строках редко проявляются сразу: формат может корректно работать в одной локали и ломаться в другой, отображать неправильные разделители, неверно округлять числа или использовать неподходящую валютную маску.
Библиотека Globalize активно использует данные CLDR и предоставляет API для создания форматтеров. Поскольку форматтеры являются функциями, их удобно покрывать юнит-тестами.
Тестирование форматтеров решает несколько задач:
Для тестирования удобно использовать:
Пример установки Jest:
npm install --save-dev jest
Установка Globalize и CLDR:
npm install globalize cldr-data
Типичная структура:
project/
├── src/
├── tests/
│ ├── number.test.js
│ ├── currency.test.js
│ ├── date.test.js
│ └── message.test.js
└── jest.config.js
Перед запуском тестов необходимо загрузить CLDR-данные.
// tests/setupGlobalize.js
const Globalize = require("globalize");
Globalize.load(
require("cldr-data/supplemental/likelySubtags.json"),
require("cldr-data/main/en/numbers.json"),
require("cldr-data/main/en/ca-gregorian.json"),
require("cldr-data/main/en/currencies.json"),
require("cldr-data/main/ru/numbers.json"),
require("cldr-data/main/ru/ca-gregorian.json"),
require("cldr-data/main/ru/currencies.json"),
require("cldr-data/supplemental/numberingSystems.json")
);
Globalize.loadTimeZone(require("iana-tz-data"));
module.exports = Globalize;
const Globalize = require("./setupGlobalize");
describe("Number formatting", () => {
test("formats number in English locale", () => {
const g = new Globalize("en");
const formatter = g.numberFormatter();
expect(formatter(1234567.89))
.toBe("1,234,567.89");
});
test("formats number in Russian locale", () => {
const g = new Globalize("ru");
const formatter = g.numberFormatter();
expect(formatter(1234567.89))
.toBe("1 234 567,89");
});
});
Globalize использует правила ICU/CLDR, поэтому округление следует тестировать отдельно.
test("rounds number correctly", () => {
const g = new Globalize("en");
const formatter = g.numberFormatter({
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
expect(formatter(1.236)).toBe("1.24");
});
test("formats negative numbers", () => {
const g = new Globalize("en");
const formatter = g.numberFormatter();
expect(formatter(-5000)).toBe("-5,000");
});
test("formats percent values", () => {
const g = new Globalize("en");
const formatter = g.numberFormatter({
style: "percent"
});
expect(formatter(0.25)).toBe("25%");
});
Форматирование валют особенно чувствительно к локали.
test("formats USD currency", () => {
const g = new Globalize("en");
const formatter = g.currencyFormatter("USD");
expect(formatter(100))
.toBe("$100.00");
});
test("formats RUB currency in Russian locale", () => {
const g = new Globalize("ru");
const formatter = g.currencyFormatter("RUB");
expect(formatter(1500))
.toBe("1 500,00 ₽");
});
test("formats currency without decimals", () => {
const g = new Globalize("en");
const formatter = g.currencyFormatter("USD", {
minimumFractionDigits: 0,
maximumFractionDigits: 0
});
expect(formatter(500))
.toBe("$500");
});
Форматирование дат зависит от:
test("formats short date", () => {
const g = new Globalize("en");
const formatter = g.dateFormatter({
datetime: "short"
});
const result = formatter(new Date(2025, 4, 20));
expect(result).toContain("5/");
});
test("formats Russian date", () => {
const g = new Globalize("ru");
const formatter = g.dateFormatter({
date: "medium"
});
const result = formatter(new Date(2025, 0, 15));
expect(result).toContain("2025");
});
Ошибки временных зон являются одной из самых частых проблем.
test("formats UTC date", () => {
const g = new Globalize("en");
const formatter = g.dateFormatter({
datetime: "medium",
timeZone: "UTC"
});
const date = new Date(Date.UTC(2025, 0, 1, 12, 0));
const result = formatter(date);
expect(result).toContain("12:00");
});
test("formats custom date pattern", () => {
const g = new Globalize("en");
const formatter = g.dateFormatter({
raw: "yyyy-MM-dd"
});
expect(
formatter(new Date(2025, 6, 10))
).toBe("2025-07-10");
});
Globalize поддерживает множественные формы.
test("selects plural category", () => {
const g = new Globalize("en");
const plural = g.pluralGenerator();
expect(plural(1)).toBe("one");
expect(plural(5)).toBe("other");
});
Русский язык содержит сложные правила множественного числа.
test("handles Russian plurals", () => {
const g = new Globalize("ru");
const plural = g.pluralGenerator();
expect(plural(1)).toBe("one");
expect(plural(2)).toBe("few");
expect(plural(5)).toBe("many");
});
MessageFormatter часто используется совместно с ICU MessageFormat.
Globalize.loadMessages({
en: {
hello: "Hello, {name}"
}
});
test("formats localized message", () => {
const g = new Globalize("en");
const formatter = g.messageFormatter("hello");
expect(
formatter({ name: "John" })
).toBe("Hello, John");
});
Globalize.loadMessages({
en: {
items:
"{count, plural, one {# item} other {# items}}"
}
});
test("formats pluralized message", () => {
const g = new Globalize("en");
const formatter = g.messageFormatter("items");
expect(formatter({ count: 1 }))
.toBe("1 item");
expect(formatter({ count: 5 }))
.toBe("5 items");
});
Некоторые приложения используют fallback-механизм.
test("falls back to English locale", () => {
const g = new Globalize("fr");
const formatter = g.numberFormatter();
expect(formatter(1000))
.toContain("1");
});
test("throws for missing message", () => {
const g = new Globalize("en");
expect(() => {
g.messageFormatter("missing");
}).toThrow();
});
test("throws on invalid locale", () => {
expect(() => {
new Globalize("invalid-locale");
}).toThrow();
});
test("handles null safely", () => {
const g = new Globalize("en");
const formatter = g.numberFormatter();
expect(() => formatter(null))
.not.toThrow();
});
test("formats NaN", () => {
const g = new Globalize("en");
const formatter = g.numberFormatter();
expect(formatter(NaN))
.toContain("NaN");
});
test("formats Infinity", () => {
const g = new Globalize("en");
const formatter = g.numberFormatter();
expect(formatter(Infinity))
.toContain("∞");
});
Snapshot-тесты полезны для контроля форматирования.
test("matches number snapshot", () => {
const g = new Globalize("ru");
const formatter = g.numberFormatter();
expect(
formatter(123456.78)
).toMatchSnapshot();
});
Первый запуск создаёт snapshot:
exports[`matches number snapshot 1`] =
`"123 456,78"`;
Snapshot-тесты для локализации требуют осторожности:
По этой причине snapshot-тесты рекомендуется использовать только для стабильных сценариев.
Некоторые локали используют специальные Unicode-пробелы.
Например:
Для избежания ложных падений тестов применяют нормализацию.
function normalize(str) {
return str.replace(/\s/g, " ");
}
Использование:
expect(
normalize(formatter(1000))
).toBe("1 000");
При больших тестовых наборах загрузка полного CLDR замедляет тестирование.
Оптимизация:
Globalize.load(
require("cldr-data/main/en/numbers.json")
);
Тесты должны загружать только необходимые данные.
Иногда форматтеры оборачиваются собственными сервисами.
Пример:
export function formatPrice(value) {
const g = new Globalize("en");
return g.currencyFormatter("USD")(value);
}
Тест:
test("formats price correctly", () => {
expect(formatPrice(50))
.toBe("$50.00");
});
При необходимости можно подменять методы.
jest.mock("globalize", () => {
return jest.fn().mockImplementation(() => ({
numberFormatter: () => value => `mock:${value}`
}));
});
Иногда важно контролировать скорость создания форматтеров.
function renderPrice(price) {
const g = new Globalize("en");
return g.currencyFormatter("USD")(price);
}
Форматтер создаётся при каждом вызове.
test("reuses formatter", () => {
const g = new Globalize("en");
const formatter = g.currencyFormatter("USD");
const start = performance.now();
for (let i = 0; i < 10000; i++) {
formatter(i);
}
const end = performance.now();
expect(end - start).toBeLessThan(100);
});
Часто используется memoization.
const cache = new Map();
function getFormatter(locale) {
if (!cache.has(locale)) {
cache.set(
locale,
new Globalize(locale).numberFormatter()
);
}
return cache.get(locale);
}
Тест:
test("returns cached formatter", () => {
const a = getFormatter("en");
const b = getFormatter("en");
expect(a).toBe(b);
});
Jest поддерживает table-driven testing.
describe.each([
["en", "1,000.5"],
["ru", "1 000,5"]
])("locale %s", (locale, expected) => {
test("formats correctly", () => {
const g = new Globalize(locale);
const formatter = g.numberFormatter();
expect(formatter(1000.5))
.toBe(expected);
});
});
describe.each([
["USD", "$10.00"],
["EUR", "€10.00"]
])("currency %s", (currency, expected) => {
test("formats currency", () => {
const g = new Globalize("en");
const formatter =
g.currencyFormatter(currency);
expect(formatter(10))
.toBe(expected);
});
});
Во многих приложениях Globalize скрывается за abstraction layer.
export class I18nService {
constructor(locale) {
this.g = new Globalize(locale);
}
formatMoney(value) {
return this.g
.currencyFormatter("USD")(value);
}
}
Тест:
test("formats money via service", () => {
const service = new I18nService("en");
expect(service.formatMoney(99))
.toBe("$99.00");
});
Регрессионные тесты фиксируют ранее найденные ошибки.
test("does not duplicate currency symbol", () => {
const g = new Globalize("en");
const formatter =
g.currencyFormatter("USD");
expect(formatter(10))
.not.toBe("$$10.00");
});
После обновления:
необходимо запускать полный набор тестов локализации.
Особенно важно проверять:
Хорошая практика:
tests/
├── formatters/
│ ├── number/
│ ├── currency/
│ ├── date/
│ └── message/
├── locales/
├── regression/
└── snapshots/
Ошибка:
expect(result).toBe("1 000");
В строке может использоваться Unicode NBSP.
E_MISSING_CLDR
Причина — забытые JSON-файлы.
expect(formatter(new Date()))
Текущая дата зависит от:
Антипаттерн:
new Date()
Лучше:
new Date(Date.UTC(2025, 0, 1))
Надёжные тесты форматтеров обладают следующими свойствами:
Юнит-тест:
formatter(1000)
Интеграционный тест:
render(<Price value={1000} />)
Юнит-тесты проверяют сам форматтер, интеграционные — взаимодействие UI и локализации.
Иногда приложение поддерживает кастомные locale aliases.
const localeMap = {
ua: "uk",
kz: "kk"
};
Тест:
test("maps locale aliases", () => {
expect(localeMap.ua).toBe("uk");
});
Тесты Globalize обычно включаются в:
Пример:
npm test
или:
jest tests/formatters
При поддержке десятков локалей полезно:
export default [
{
locale: "en",
input: 1000.5,
output: "1,000.5"
},
{
locale: "ru",
input: 1000.5,
output: "1 000,5"
}
];
Тест:
fixtures.forEach(item => {
test(item.locale, () => {
const g = new Globalize(item.locale);
const formatter = g.numberFormatter();
expect(
formatter(item.input)
).toBe(item.output);
});
});