Серверный рендеринг (SSR, Server-Side Rendering) играет важную роль в проектах с многоязычным интерфейсом. При использовании клиентской локализации браузер получает страницу на языке по умолчанию, после чего JavaScript загружает переводы и обновляет содержимое. Такой подход может вызывать визуальное мерцание текста, ухудшать показатели производительности и создавать проблемы для поисковых систем.
I18next предоставляет инструменты для полноценной серверной локализации, позволяя формировать HTML сразу на нужном языке еще до передачи страницы клиенту.
Основные преимущества SSR:
Рассмотрим типичный сценарий без SSR.
HTML, отправленный сервером:
<h1>Loading...</h1>
После инициализации I18next:
document.querySelector("h1").textContent = i18next.t("home.title");
Пользователь может увидеть:
Подобный эффект называется FOUC (Flash of Untranslated Content) или FOIT для локализованного контента.
При большом количестве переводов ситуация становится более заметной:
await i18next.init({
lng: "de",
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json"
}
});
До завершения загрузки переводов интерфейс остается неполным.
SSR устраняет данную проблему, поскольку переводы внедряются в HTML заранее.
Типичный жизненный цикл выглядит следующим образом:
Схематично процесс можно представить так:
Request
↓
Language Detection
↓
Load Resources
↓
I18next Init
↓
SSR Render
↓
HTML Response
↓
Hydration
При 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:
app.get("/:lng/home", async (req, res) => {
const lng = req.params.lng;
});
Пример работы с заголовком:
const language =
req.headers["accept-language"]
?.split(",")[0]
?.split("-")[0] || "en";
Для более сложной логики обычно применяется 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 умеет определять язык по:
Конфигурация:
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 ресурсы загружаются непосредственно с диска без дополнительных сетевых запросов.
Простейшая реализация может выглядеть следующим образом.
Инициализация:
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);
});
Страница будет сформирована сразу на нужном языке.
Для 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
В больших приложениях используются 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"
/>
Поисковые роботы получают локализованные данные сразу при обходе страницы.
Социальные сети используют специальные метатеги.
Пример:
<meta
property="og:title"
content="Willkommen"
/>
<meta
property="og:description"
content="Startseite"
/>
Сервер может формировать их через I18next:
i18n.t("og.title");
i18n.t("og.description");
Это особенно важно для многоязычных лендингов и интернет-магазинов.
Для 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
});
Для поисковых систем необходимо указывать языковые версии страницы.
Пример:
<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"
]
});
В результате ресурсы уже находятся в памяти приложения и доступны для последующих запросов.
Конфигурация:
fallbackLng: "en"
Если перевод отсутствует:
{
"title": "Welcome"
}
а в немецком файле ключа нет:
{}
результат:
i18n.t("title");
будет:
Welcome
SSR гарантирует, что пользователь увидит корректное содержимое даже при неполном переводе проекта.
Для проектов на Next.js I18next обычно интегрируется через пакет next-i18next.
Особенности подхода:
Типичная загрузка переводов:
export async function getServerSideProps({
locale
}) {
return {
props: {
...(await serverSideTranslations(
locale,
["common"]
))
}
};
}
Переводы становятся доступны еще до рендеринга страницы.
В Remix локализация обычно определяется на сервере внутри loader-функций.
Пример:
export async function loader({
request
}) {
const language =
detectLanguage(request);
return json({
language
});
}
Затем экземпляр I18next создается для каждого запроса отдельно.
Такой подход хорошо сочетается с архитектурой Remix, ориентированной на серверный рендеринг.
Для достижения максимальной производительности рекомендуется:
При соблюдении этих принципов I18next обеспечивает полноценную серверную локализацию, корректную индексацию многоязычного контента и высокую производительность современных SSR-приложений.