Gatsby и SSG

В статической генерации сайтов (SSG), реализованной в Gatsby, интернационализация требует строгого разделения этапов сборки и выполнения. FormatJS в этой архитектуре выступает как слой форматирования сообщений и управления локалями, работающий как на этапе билда, так и в клиентской гидратации.

Ключевая особенность Gatsby — генерация HTML на этапе сборки для каждого маршрута. Это означает, что локализация должна быть определена до рендеринга страницы, а не после загрузки JavaScript в браузере.

В связке Gatsby + FormatJS используется модель:

  • сбор сообщений на этапе build-time;
  • генерация отдельных HTML-деревьев для каждой локали;
  • передача локализованных словарей в React-контекст;
  • повторная гидратация с тем же набором данных на клиенте.

Модель сообщений FormatJS в статической генерации

FormatJS основан на ICU Message Syntax, что позволяет описывать:

  • множественные формы;
  • выбор по полу или категориям;
  • числовое форматирование;
  • даты и временные зоны.

Типичный формат сообщений:

{
  "app.title": "Платформа",
  "items.count": "{count, plural, one {# элемент} few {# элемента} many {# элементов} other {# элемента}}"
}

При SSG эти сообщения компилируются в статические JSON-словари, которые затем встраиваются в результат сборки Gatsby.


Структура локалей в проекте Gatsby

Обычно локализация организуется через следующую структуру:

/locales
  /ru
    messages.json
  /en
    messages.json
  /de
    messages.json

Каждый файл содержит набор ключей FormatJS.

На этапе сборки Gatsby эти файлы становятся источником данных для генерации страниц.


Интеграция через react-intl и FormatJS

Основной слой интеграции реализуется через 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 становится ключевым параметром для загрузки нужного набора сообщений.


Загрузка сообщений на этапе build-time

Для каждой страницы 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 CLI

FormatJS предоставляет инструменты извлечения сообщений из исходного кода:

formatjs extract "src/**/*.js" --out-file messages.json

Этот процесс:

  • сканирует JSX и JavaScript;
  • извлекает defineMessages, FormattedMessage;
  • формирует единый словарь ключей.

Пример исходного кода:

import { defineMessages } from "react-intl";

const messages = defineMessages({
  title: {
    id: "home.title",
    defaultMessage: "Главная"
  }
});

Результат попадает в JSON, который затем используется Gatsby при генерации страниц.


Синхронизация SSR и клиентской гидратации

В SSG критична идентичность HTML, сгенерированного на сервере, и результата React на клиенте. FormatJS требует одинакового набора сообщений и одинакового locale.

Если возникает расхождение:

  • сервер генерирует один язык;
  • клиент пытается гидратировать другой;
  • появляется mismatch hydration.

Чтобы этого избежать, locale фиксируется через pageContext.


Форматирование дат и чисел в статическом HTML

FormatJS использует Intl API браузера и Node.js:

import { FormattedDate } from "react-intl";

<FormattedDate
  value={Date.now()}
  year="numeric"
  month="long"
  day="2-digit"
/>

В SSG это превращается в строку уже на этапе сборки, что снижает нагрузку на клиент.

Особенность Gatsby: результат форматирования может быть зафиксирован в HTML, что делает страницу полностью статической без необходимости вычислений в браузере.


Разделение bundle по локалям

При большом количестве языков возникает проблема размера бандла. Решение — код-сплиттинг сообщений:

  • загрузка JSON только для текущей локали;
  • использование import() внутри wrapPageElement;
  • кэширование через Gatsby build pipeline.

Пример динамической загрузки:

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 сообщения и особенности рендера в Gatsby

ICU-синтаксис FormatJS компилируется в JavaScript-структуры, которые интерпретируются во время выполнения или сборки.

Особенно важны конструкции:

  • pluralization;
  • select;
  • dateTime formatting.

Пример:

<FormattedMessage
  id="cart.items"
  values={{ count: 3 }}
/>

Результат зависит от локали, заданной в IntlProvider.


Обработка маршрутов и локалей через gatsby-node

При построении сайта 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
      }
    });
  });
};

Такой подход дублирует страницы под каждую локаль.


Производительность при использовании FormatJS в SSG

SSG устраняет необходимость вычисления сообщений в runtime, но сохраняет несколько важных аспектов:

  • уменьшение client-side JS за счёт предрендеринга;
  • отсутствие необходимости загрузки runtime переводов;
  • ускорение TTFB за счёт готового HTML;
  • снижение нагрузки на Intl API в браузере.

Основная цена переносится на этап сборки Gatsby.


Кэширование и стабильность сообщений

FormatJS требует стабильности идентификаторов сообщений. В SSG это критично:

  • изменение id приводит к полной регенерации страниц;
  • несоответствие словарей вызывает разрывы гидратации;
  • рекомендуется централизованное управление message IDs.

Динамические значения и контекст страницы

Gatsby передаёт контекст в шаблоны страниц:

const Page = ({ pageContext }) => {
  const { locale } = pageContext;

  return <div>{locale}</div>;
};

Этот же контекст используется FormatJS для выбора сообщений.

Таким образом достигается единая точка управления локализацией между сборкой и рендерингом.


Особенности масштабирования мультиязычных сайтов

При росте количества локалей возникают архитектурные ограничения:

  • увеличение времени сборки Gatsby;
  • рост количества HTML-файлов;
  • дублирование страниц;
  • увеличение объёма message catalog.

FormatJS остаётся стабильным слоем, но требует строгой организации:

  • разделение доменных словарей;
  • lazy-loading переводов;
  • сегментация build pipeline.

Использование fallback-локалей

FormatJS поддерживает fallback-цепочки:

  • отсутствующий ключ берётся из базовой локали;
  • обычно используется en как fallback.

В Gatsby это реализуется на уровне загрузки сообщений:

const messages = {
  ...enMessages,
  ...ruMessages
};

Такой merge обеспечивает устойчивость интерфейса при неполных переводах.