SSR и индексация

Серверный рендеринг (SSR, Server-Side Rendering) играет важную роль в проектах с многоязычным интерфейсом. При использовании клиентской локализации браузер получает страницу на языке по умолчанию, после чего JavaScript загружает переводы и обновляет содержимое. Такой подход может вызывать визуальное мерцание текста, ухудшать показатели производительности и создавать проблемы для поисковых систем.

I18next предоставляет инструменты для полноценной серверной локализации, позволяя формировать HTML сразу на нужном языке еще до передачи страницы клиенту.

Основные преимущества SSR:

  • отсутствие смены языка после загрузки страницы;
  • улучшение пользовательского опыта;
  • корректная индексация локализованного контента;
  • повышение показателей Core Web Vitals;
  • возможность генерации метатегов на нужном языке;
  • поддержка многоязычных URL.

Проблемы клиентской локализации

Рассмотрим типичный сценарий без SSR.

HTML, отправленный сервером:

<h1>Loading...</h1>

После инициализации I18next:

document.querySelector("h1").textContent = i18next.t("home.title");

Пользователь может увидеть:

  1. загрузочный текст;
  2. текст на языке по умолчанию;
  3. текст на выбранном языке.

Подобный эффект называется FOUC (Flash of Untranslated Content) или FOIT для локализованного контента.

При большом количестве переводов ситуация становится более заметной:

await i18next.init({
    lng: "de",
    backend: {
        loadPath: "/locales/{{lng}}/{{ns}}.json"
    }
});

До завершения загрузки переводов интерфейс остается неполным.

SSR устраняет данную проблему, поскольку переводы внедряются в HTML заранее.


Общая схема SSR с I18next

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

  1. Получение HTTP-запроса.
  2. Определение языка пользователя.
  3. Загрузка переводов.
  4. Инициализация экземпляра I18next.
  5. Рендеринг HTML.
  6. Отправка результата клиенту.
  7. Гидратация приложения на клиенте.

Схематично процесс можно представить так:

Request
   ↓
Language Detection
   ↓
Load Resources
   ↓
I18next Init
   ↓
SSR Render
   ↓
HTML Response
   ↓
Hydration

Создание отдельного экземпляра I18next

При SSR нельзя использовать глобальный экземпляр библиотеки для всех запросов одновременно.

Неправильно:

import i18next from "i18next";

await i18next.changeLanguage("de");

Если одновременно обрабатываются разные пользователи, может возникнуть конфликт языков.

Правильный подход:

import i18next from "i18next";

const instance = i18next.createInstance();

await instance.init({
    lng: "de",
    resources
});

Каждый HTTP-запрос получает собственную изолированную конфигурацию.

Пример:

app.get("*", async (req, res) => {
    const i18n = i18next.createInstance();

    await i18n.init({
        lng: req.language,
        resources
    });

    const html = renderPage(i18n);

    res.send(html);
});

Определение языка на сервере

Сервер должен определить предпочтительный язык пользователя до рендеринга страницы.

Источники определения языка:

  • URL;
  • поддомен;
  • cookie;
  • HTTP-заголовок Accept-Language;
  • пользовательские настройки профиля.

Пример анализа URL:

app.get("/:lng/home", async (req, res) => {
    const lng = req.params.lng;
});

Пример работы с заголовком:

const language =
    req.headers["accept-language"]
        ?.split(",")[0]
        ?.split("-")[0] || "en";

Для более сложной логики обычно применяется middleware.


i18next-http-middleware

Пакет значительно упрощает работу с серверной локализацией.

Установка:

npm install i18next-http-middleware

Подключение:

import middleware from "i18next-http-middleware";

Настройка:

app.use(
    middleware.handle(i18next)
);

После этого становится доступным объект:

req.language

Пример:

app.get("/", (req, res) => {
    console.log(req.language);
});

Middleware умеет определять язык по:

  • URL;
  • cookie;
  • заголовкам;
  • параметрам запроса.

Конфигурация:

detection: {
    order: [
        "path",
        "cookie",
        "header"
    ]
}

Загрузка переводов на сервере

Наиболее распространенный вариант — использование файловой системы.

Установка:

npm install i18next-fs-backend

Подключение:

import Backend from "i18next-fs-backend";

Конфигурация:

await i18next.init({
    backend: {
        loadPath:
            "./locales/{{lng}}/{{ns}}.json"
    }
});

Структура каталога:

locales/
├── en
│   └── common.json
├── de
│   └── common.json
└── fr
    └── common.json

Файл перевода:

{
    "title": "Welcome"
}

Во время SSR ресурсы загружаются непосредственно с диска без дополнительных сетевых запросов.


SSR в Express

Простейшая реализация может выглядеть следующим образом.

Инициализация:

import express from "express";
import i18next from "i18next";
import Backend from "i18next-fs-backend";

const app = express();

Обработка запроса:

app.get("/:lng", async (req, res) => {
    const i18n = i18next.createInstance();

    await i18n
        .use(Backend)
        .init({
            lng: req.params.lng,
            fallbackLng: "en",
            backend: {
                loadPath:
                    "./locales/{{lng}}/{{ns}}.json"
            }
        });

    const html = `
        <h1>${i18n.t("title")}</h1>
    `;

    res.send(html);
});

Страница будет сформирована сразу на нужном языке.


SSR и React

Для React используется пакет React I18next.

Установка:

npm install react-i18next

Серверный компонент:

function Home() {
    const { t } = useTranslation();

    return (
        <h1>{t("title")}</h1>
    );
}

На сервере:

import { renderToString } from "react-dom/server";

Рендеринг:

const html = renderToString(
    <App />
);

При корректной инициализации I18next сервер вернет готовый переведенный HTML.


Передача ресурсов клиенту

После серверного рендеринга клиент должен использовать те же данные переводов.

Частая ошибка:

await i18next.init(...);

После загрузки страницы браузер снова скачивает те же ресурсы.

Оптимизированный вариант:

<script>
window.__I18N__ = {
    lng: "de",
    resources: {
        de: {
            common: {
                title: "Willkommen"
            }
        }
    }
};
</script>

На клиенте:

await i18next.init({
    lng: window.__I18N__.lng,
    resources: window.__I18N__.resources
});

Дополнительный запрос к серверу больше не требуется.


Гидратация и синхронизация языков

После SSR React выполняет гидратацию.

Если сервер использовал язык:

de

а клиент запускается с:

en

возникнет несоответствие дерева компонентов.

Пример ошибки:

Hydration failed because the initial UI does not match

Поэтому серверный и клиентский экземпляры должны использовать одинаковые настройки:

lng: initialLanguage

и одинаковые ресурсы:

resources: initialResources

Пространства имен и SSR

В больших приложениях используются namespaces.

Структура:

locales/
└── en
    ├── common.json
    ├── home.json
    └── profile.json

Инициализация:

await i18n.init({
    ns: ["common", "home"],
    defaultNS: "common"
});

В компоненте:

const { t } = useTranslation("home");

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

Это уменьшает объем HTML и JSON-данных.


Генерация локализованных метатегов

Одна из важнейших причин использования SSR — корректное формирование SEO-данных.

Пример:

const title = i18n.t("meta.title");
const description = i18n.t("meta.description");

Генерация HTML:

<title>Produktseite</title>

<meta
    name="description"
    content="Beschreibung des Produkts"
/>

Поисковые роботы получают локализованные данные сразу при обходе страницы.


Локализация Open Graph

Социальные сети используют специальные метатеги.

Пример:

<meta
    property="og:title"
    content="Willkommen"
/>

<meta
    property="og:description"
    content="Startseite"
/>

Сервер может формировать их через I18next:

i18n.t("og.title");
i18n.t("og.description");

Это особенно важно для многоязычных лендингов и интернет-магазинов.


Локализованные URL

Для SEO рекомендуется использовать отдельные адреса для каждого языка.

Примеры:

/en/products
/de/products
/fr/products

Либо:

en.example.com
de.example.com
fr.example.com

Получение языка:

const lng = req.params.lng;

После этого язык используется при инициализации I18next:

await i18n.init({
    lng
});

Тег hreflang

Для поисковых систем необходимо указывать языковые версии страницы.

Пример:

<link
    rel="alternate"
    hreflang="en"
    href="https://site.com/en"
/>

<link
    rel="alternate"
    hreflang="de"
    href="https://site.com/de"
/>

<link
    rel="alternate"
    hreflang="fr"
    href="https://site.com/fr"
/>

Такие ссылки помогают поисковым системам правильно выбирать локализованную версию документа.


Индексация многоязычного контента

При использовании SSR поисковый робот получает уже переведенную страницу:

<h1>Willkommen</h1>

вместо:

<h1>{{title}}</h1>

или:

<h1>Loading...</h1>

Это позволяет:

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

Проблемы с динамической загрузкой переводов

Некоторые проекты загружают переводы только после открытия страницы.

Пример:

backend: {
    loadPath:
        "/api/translations/{{lng}}"
}

При SSR это может привести к дополнительным задержкам.

Лучше загружать переводы до начала рендеринга:

await i18n.init({
    lng,
    preload: ["en", "de", "fr"]
});

или использовать файловый backend.


Кэширование переводов

При высоких нагрузках полезно кэшировать ресурсы локализации.

Пример кэша в памяти:

const cache = new Map();

Получение данных:

if (cache.has(language)) {
    return cache.get(language);
}

После загрузки:

cache.set(language, resources);

Это снижает количество операций чтения файловой системы.


Предварительная загрузка языков

Для популярных языков можно выполнить загрузку заранее.

Пример:

await i18next.init({
    preload: [
        "en",
        "de",
        "fr",
        "es"
    ]
});

В результате ресурсы уже находятся в памяти приложения и доступны для последующих запросов.


Работа с fallback-языками при SSR

Конфигурация:

fallbackLng: "en"

Если перевод отсутствует:

{
    "title": "Welcome"
}

а в немецком файле ключа нет:

{}

результат:

i18n.t("title");

будет:

Welcome

SSR гарантирует, что пользователь увидит корректное содержимое даже при неполном переводе проекта.


SSR в Next.js

Для проектов на Next.js I18next обычно интегрируется через пакет next-i18next.

Особенности подхода:

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

Типичная загрузка переводов:

export async function getServerSideProps({
    locale
}) {
    return {
        props: {
            ...(await serverSideTranslations(
                locale,
                ["common"]
            ))
        }
    };
}

Переводы становятся доступны еще до рендеринга страницы.


SSR в Remix

В Remix локализация обычно определяется на сервере внутри loader-функций.

Пример:

export async function loader({
    request
}) {
    const language =
        detectLanguage(request);

    return json({
        language
    });
}

Затем экземпляр I18next создается для каждого запроса отдельно.

Такой подход хорошо сочетается с архитектурой Remix, ориентированной на серверный рендеринг.


Производительность SSR-приложений с I18next

Для достижения максимальной производительности рекомендуется:

  • использовать отдельный экземпляр I18next для каждого запроса;
  • передавать ресурсы локализации в HTML;
  • минимизировать количество namespaces;
  • кэшировать переводы;
  • использовать preload для популярных языков;
  • избегать повторной загрузки ресурсов после гидратации;
  • генерировать метатеги на сервере;
  • применять локализованные URL и hreflang;
  • выполнять определение языка до начала рендеринга;
  • хранить переводы в быстром backend или памяти приложения.

При соблюдении этих принципов I18next обеспечивает полноценную серверную локализацию, корректную индексацию многоязычного контента и высокую производительность современных SSR-приложений.