В статической генерации сайтов (SSG), реализованной в Gatsby, интернационализация требует строгого разделения этапов сборки и выполнения. FormatJS в этой архитектуре выступает как слой форматирования сообщений и управления локалями, работающий как на этапе билда, так и в клиентской гидратации.
Ключевая особенность Gatsby — генерация HTML на этапе сборки для каждого маршрута. Это означает, что локализация должна быть определена до рендеринга страницы, а не после загрузки JavaScript в браузере.
В связке Gatsby + FormatJS используется модель:
FormatJS основан на ICU Message Syntax, что позволяет описывать:
Типичный формат сообщений:
{
"app.title": "Платформа",
"items.count": "{count, plural, one {# элемент} few {# элемента} many {# элементов} other {# элемента}}"
}
При SSG эти сообщения компилируются в статические JSON-словари, которые затем встраиваются в результат сборки Gatsby.
Обычно локализация организуется через следующую структуру:
/locales
/ru
messages.json
/en
messages.json
/de
messages.json
Каждый файл содержит набор ключей FormatJS.
На этапе сборки Gatsby эти файлы становятся источником данных для генерации страниц.
Основной слой интеграции реализуется через React Intl.
В Gatsby используется обёртка корневого компонента:
import React from "react";
import { IntlProvider } from "react-intl";
import messages from "./locales/ru/messages.json";
export const wrapRootElement = ({ element }) => (
<IntlProvider locale="ru" messages={messages}>
{element}
</IntlProvider>
);
На уровне SSG этот провайдер внедряется в
gatsby-browser.js и gatsby-ssr.js, чтобы
обеспечить идентичный результат на сервере и клиенте.
Gatsby позволяет создавать страницы динамически через API
createPages. При мультилингвальной архитектуре каждая
локаль становится частью маршрутизации:
/ru/about
/en/about
/de/about
Пример генерации страниц:
exports.createPages = async ({ actions }) => {
const { createPage } = actions;
const locales = ["ru", "en"];
locales.forEach(locale => {
createPage({
path: `/${locale}/about`,
component: require.resolve("./src/templates/about.js"),
context: {
locale
}
});
});
};
Здесь context.locale становится ключевым параметром для
загрузки нужного набора сообщений.
Для каждой страницы Gatsby может загружать отдельный набор сообщений:
import ru from "../locales/ru/messages.json";
import en from "../locales/en/messages.json";
const messagesByLocale = {
ru,
en
};
export const wrapPageElement = ({ element, props }) => {
const locale = props.pageContext.locale;
return (
<IntlProvider locale={locale} messages={messagesByLocale[locale]}>
{element}
</IntlProvider>
);
};
Такой подход гарантирует, что каждая HTML-страница уже содержит корректно локализованный контент без дополнительного запроса к серверу.
FormatJS предоставляет инструменты извлечения сообщений из исходного кода:
formatjs extract "src/**/*.js" --out-file messages.json
Этот процесс:
defineMessages,
FormattedMessage;Пример исходного кода:
import { defineMessages } from "react-intl";
const messages = defineMessages({
title: {
id: "home.title",
defaultMessage: "Главная"
}
});
Результат попадает в JSON, который затем используется Gatsby при генерации страниц.
В SSG критична идентичность HTML, сгенерированного на сервере, и
результата React на клиенте. FormatJS требует одинакового набора
сообщений и одинакового locale.
Если возникает расхождение:
Чтобы этого избежать, locale фиксируется через
pageContext.
FormatJS использует Intl API браузера и Node.js:
import { FormattedDate } from "react-intl";
<FormattedDate
value={Date.now()}
year="numeric"
month="long"
day="2-digit"
/>
В SSG это превращается в строку уже на этапе сборки, что снижает нагрузку на клиент.
Особенность Gatsby: результат форматирования может быть зафиксирован в HTML, что делает страницу полностью статической без необходимости вычислений в браузере.
При большом количестве языков возникает проблема размера бандла. Решение — код-сплиттинг сообщений:
import() внутри
wrapPageElement;Пример динамической загрузки:
export const wrapPageElement = async ({ element, props }) => {
const locale = props.pageContext.locale;
const messages = await import(`./locales/${locale}/messages.json`);
return (
<IntlProvider locale={locale} messages={messages.default}>
{element}
</IntlProvider>
);
};
ICU-синтаксис FormatJS компилируется в JavaScript-структуры, которые интерпретируются во время выполнения или сборки.
Особенно важны конструкции:
Пример:
<FormattedMessage
id="cart.items"
values={{ count: 3 }}
/>
Результат зависит от локали, заданной в
IntlProvider.
При построении сайта Gatsby использует Node API, где определяется структура локализованных маршрутов.
exports.onCreateP age = ({ page, actions }) => {
const { createPage, deletePage } = actions;
deletePage(page);
["ru", "en"].forEach(locale => {
createPage({
...page,
path: `/${locale}${page.path}`,
context: {
locale
}
});
});
};
Такой подход дублирует страницы под каждую локаль.
SSG устраняет необходимость вычисления сообщений в runtime, но сохраняет несколько важных аспектов:
Основная цена переносится на этап сборки Gatsby.
FormatJS требует стабильности идентификаторов сообщений. В SSG это критично:
id приводит к полной регенерации
страниц;Gatsby передаёт контекст в шаблоны страниц:
const Page = ({ pageContext }) => {
const { locale } = pageContext;
return <div>{locale}</div>;
};
Этот же контекст используется FormatJS для выбора сообщений.
Таким образом достигается единая точка управления локализацией между сборкой и рендерингом.
При росте количества локалей возникают архитектурные ограничения:
FormatJS остаётся стабильным слоем, но требует строгой организации:
FormatJS поддерживает fallback-цепочки:
en как fallback.В Gatsby это реализуется на уровне загрузки сообщений:
const messages = {
...enMessages,
...ruMessages
};
Такой merge обеспечивает устойчивость интерфейса при неполных переводах.