Проблематика стабильности локализованного форматирования возникает из-за того, что результат работы Intl API зависит не только от входных данных, но и от окружения выполнения: версии ICU, операционной системы, настроек локали и временной зоны. Это делает прямое сравнение строк форматирования ненадёжным при использовании классических snapshot-тестов без стабилизации контекста.
Intl API в JavaScript построен на ICU (International Components for Unicode), и итоговое представление данных формируется на основе локализованных правил. Даже при одинаковом коде результат может отличаться:
Пример потенциально нестабильного форматирования:
const formatter = new Intl.DateTimeFormat("ru-RU", {
dateStyle: "full",
timeStyle: "long"
});
formatter.format(new Date("2024-01-01T10:00:00Z"));
Результат может отличаться в зависимости от timezone и версии ICU.
Snapshot-тестирование фиксирует строковый результат функции и сравнивает его с сохранённым эталоном. В контексте Intl API это означает сохранение результата форматирования чисел, дат или строк в локализованном виде.
Пример с Jest:
test("форматирование даты в ru-RU", () => {
const formatter = new Intl.DateTimeFormat("ru-RU", {
year: "numeric",
month: "long",
day: "numeric"
});
const result = formatter.format(new Date("2024-01-01T00:00:00Z"));
expect(result).toMatchSnapshot();
});
Первый запуск сохраняет строку в snapshot-файл, последующие — сравнивают результат.
Intl.NumberFormat используется для валют, процентов, компактных чисел:
const formatter = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR"
});
formatter.format(123456.78);
Snapshot фиксирует результат вида:
"123.456,78 €"
При этом важно учитывать, что изменение валютных правил или ICU может менять группировку и символы.
DateTimeFormat наиболее чувствителен к окружению:
const formatter = new Intl.DateTimeFormat("en-GB", {
weekday: "long",
year: "numeric",
month: "short",
day: "2-digit"
});
formatter.format(new Date("2025-05-27T12:00:00Z"));
Даже при одинаковом коде результат может зависеть от временной зоны:
Явное указание локали устраняет часть неопределённости:
new Intl.NumberFormat("en-US")
new Intl.DateTimeFormat("en-US")
Критический фактор для DateTimeFormat:
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: "UTC"
});
Без этого snapshot может ломаться при запуске на разных CI-агентах.
Для стабильности дат используется фиксация времени:
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date("2024-01-01T00:00:00Z"));
});
afterAll(() => {
jest.useRealTimers();
});
Это устраняет влияние текущей даты.
В реальных проектах часто применяется слой нормализации, чтобы минимизировать влияние ICU-версий.
Intl часто использует NBSP (\u00A0):
function normalizeIntlString(str) {
return str.replace(/\u00A0/g, " ");
}
Snapshot:
expect(normalizeIntlString(result)).toMatchSnapshot();
Некоторые среды по-разному трактуют разделители:
function normalizeNumberFormatting(str) {
return str
.replace(/\u202F/g, " ")
.replace(/\u00A0/g, " ");
}
RelativeTimeFormat особенно зависит от языковых данных ICU:
const rtf = new Intl.RelativeTimeFormat("ru", { numeric: "auto" });
rtf.format(-1, "day");
rtf.format(1, "month");
Возможные результаты:
Snapshot-тестирование здесь чувствительно к изменениям языковых пакетов.
Intl.Collator используется для локализованной сортировки:
const collator = new Intl.Collator("de", { sensitivity: "base" });
["ä", "a", "z"].sort(collator.compare);
Snapshot фиксирует порядок массива, но результат может меняться при обновлении ICU.
Разные CI-агенты могут иметь:
Это приводит к «ложным падениям» snapshot-тестов.
DateTimeFormat может изменять:
Особенно критично при переходах DST.
Обновления Unicode могут менять:
Часто создаётся единый форматтер-слой:
export const formatDate = (date) =>
new Intl.DateTimeFormat("en-US", {
timeZone: "UTC"
}).format(date);
Snapshot тестируется уже результат функции, а не напрямую Intl.
Используются строго фиксированные локали:
Без динамического выбора локали.
Бизнес-логика тестируется отдельно от локализации, чтобы snapshot покрывал только финальный слой представления.
Inline snapshots позволяют фиксировать результат прямо в тесте:
expect(formatter.format(1000)).toMatchInlineSnapshot(`"1,000"`);
Это упрощает ревью изменений форматирования, но усиливает зависимость тестов от ICU.
Intl можно абстрагировать:
function createFormatters({ locale, timeZone }) {
return {
date: new Intl.DateTimeFormat(locale, { timeZone }),
number: new Intl.NumberFormat(locale)
};
}
Тесты фиксируют конфигурацию, снижая вариативность окружения.
При обновлении runtime snapshot может массово измениться без изменения кода. В таких случаях:
Это особенно важно для финансовых и дата-интенсивных интерфейсов, где Intl.NumberFormat и Intl.DateTimeFormat влияют на отображение критических данных.