Компоненты, использующие международализацию через FormatJS, обычно
зависят от контекста IntlProvider, набора сообщений и
текущей локали. В тестовой среде это создаёт дополнительный слой
сложности: поведение UI становится зависимым не только от входных
пропсов, но и от локализационного окружения.
Ключевая особенность заключается в том, что FormatJS использует
ICU-сообщения, форматирование дат, чисел и валют через стандартный
Intl. Это означает, что тесты должны учитывать
детерминированность вывода, иначе результаты становятся
нестабильными.
Основные зависимости тестируемого компонента:
locale)messages)date, number,
timeIntlProviderНа практике тестирование компонентов с i18n сводится к созданию стабильного окружения, в котором локализация полностью контролируется.
Типичный подход — создание обёртки над рендером:
import { IntlProvider } from "react-intl";
import { render } from "@testing-library/react";
const messages = {
ru: {
greeting: "Привет, {name}",
unread: "У вас {count} сообщений",
},
};
function renderWithIntl(ui, { locale = "ru", messages: msg = messages.ru } = {}) {
return render(
<IntlProvider locale={locale} messages={msg}>
{ui}
</IntlProvider>
);
}
Такой слой позволяет:
formatMessageНаиболее прямой способ работы с FormatJS в компонентах — через
useIntl() и formatMessage.
Компонент:
import { useIntl } from "react-intl";
export function Greeting({ name }) {
const intl = useIntl();
return (
<div>
{intl.formatMessage(
{ id: "greeting" },
{ name }
)}
</div>
);
}
Тест:
test("renders formatted greeting", () => {
const { getByText } = renderWithIntl(<Greeting name="Ivan" />);
getByText("Привет, Ivan");
});
Важный аспект: тестируется не структура ICU, а конечный результат форматирования. Это снижает хрупкость тестов при изменении текстов.
FormatJS активно использует параметры, и именно они чаще всего становятся источником ошибок.
test("renders message with plural form", () => {
const messages = {
unread: "У вас {count, plural, one {# сообщение} few {# сообщения} many {# сообщений}}",
};
const { getByText } = renderWithIntl(
<Unread count={5} />,
{ messages }
);
getByText("У вас 5 сообщений");
});
Здесь важно не проверять ICU-структуру, а результат ветвления plural rules.
FormatJS опирается на Intl.DateTimeFormat и
Intl.NumberFormat. Это создаёт потенциальную нестабильность
тестов из-за различий окружений CI и локальных машин.
Компонент:
import { useIntl } from "react-intl";
export function OrderDate({ date }) {
const intl = useIntl();
return (
<span>
{intl.formatDate(date, {
year: "numeric",
month: "long",
day: "2-digit",
})}
</span>
);
}
Тест становится чувствительным к локали, поэтому фиксируется окружение:
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date("2024-01-01"));
});
Либо фиксируется конкретная локаль:
const wrapper = ({ children }) => (
<IntlProvider locale="en-US">{children}</IntlProvider>
);
Проверка:
getByText("January 01, 2024");
В некоторых средах Intl может отличаться (особенно в
Node.js без полифилов). Для стабилизации используются:
@formatjs/intl-pluralrules@formatjs/intl-datetimeformat@formatjs/intl-numberformatИнициализация тестового окружения:
import "@formatjs/intl-pluralrules/polyfill";
import "@formatjs/intl-datetimeformat/polyfill";
import "@formatjs/intl-numberformat/polyfill";
import "@formatjs/intl-datetimeformat/locale-data/ru";
Это устраняет расхождения между средами выполнения.
Наиболее устойчивый подход — тестирование через поведение UI, а не через внутренние вызовы FormatJS.
Пример компонента:
export function Cart({ items }) {
const intl = useIntl();
return (
<div>
{intl.formatMessage(
{ id: "cart.total" },
{ count: items.length }
)}
</div>
);
}
Тест:
test("shows cart count", () => {
const { getByText } = renderWithIntl(
<Cart items={[1, 2, 3]} />
);
getByText("3 items");
});
Здесь проверяется пользовательское поведение, а не внутренняя реализация i18n.
Snapshot-тесты часто применяются к локализованным компонентам, но обладают высокой хрупкостью.
Проблема:
expect(container).toMatchSnapshot();
Даже незначительное изменение текста или локали приводит к массовым изменениям снапшотов.
Более устойчивый подход:
Пример стабилизации:
expect(getByTestId("label")).toHaveTextContent("Привет, Ivan");
useIntl в изоляцииИногда требуется проверить логику вне DOM.
import { createIntl, createIntlCache } from "react-intl";
const cache = createIntlCache();
const intl = createIntl(
{
locale: "ru",
messages: {
hello: "Привет",
},
},
cache
);
test("formats message outside component", () => {
expect(intl.formatMessage({ id: "hello" })).toBe("Привет");
});
Такой подход используется для:
В тестах редко используется полный набор переводов. Обычно применяется минимальный словарь:
const testMessages = {
"app.title": "Test App",
"user.name": "Name: {name}",
};
Это снижает:
FormatJS поддерживает fallback на id сообщения, если перевод отсутствует.
test("falls back to message id", () => {
const { getByText } = renderWithIntl(
<MissingMessage />
);
getByText("missing.key");
});
Это позволяет выявлять:
Иногда локаль меняется в runtime:
function LocaleSwitcher() {
const [locale, setLocale] = useState("ru");
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<button onCl ick={() => setLocale("en")}>
Switch
</button>
</IntlProvider>
);
}
Тестирование фокусируется на:
fireEvent.click(getByText("Switch"));
getByText("Hello");
Главная проблема i18n-тестирования — недетерминированность. Она возникает из-за:
Решение заключается в жёсткой фиксации среды:
localeFormatJS поддерживает вложенные конструкции:
{
id: "profile.status",
defaultMessage:
"{gender, select, male {Он онлайн} female {Она онлайн} other {Пользователь онлайн}}"
}
Тестирование:
getByText("Она онлайн");
В таких случаях проверяется не структура ICU, а конечная ветка результата.
В CI-среде особенно важно учитывать:
Практика стабилизации:
TZ=UTCТакой подход снижает вероятность ложных падений тестов при миграции окружений.