SSR и i18next в Next.js

При серверном рендеринге приложение должно возвращать уже локализованный HTML. Это влияет на:

  • SEO;
  • корректную индексацию страниц;
  • производительность первого рендера;
  • отсутствие «мигания» языка после гидратации;
  • генерацию мета-тегов и контента на нужном языке.

Связка Next.js + i18next позволяет:

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

На практике чаще всего используется библиотека:

  • i18next
  • react-i18next
  • next-i18next

Установка зависимостей

Для проекта на Next.js Pages Router:

npm install i18next react-i18next next-i18next

Дополнительно часто используются:

npm install i18next-http-backend
npm install i18next-browser-languagedetector

Структура проекта

Типичная структура:

project/
├── public/
│   └── locales/
│       ├── en/
│       │   ├── common.json
│       │   └── home.json
│       └── ru/
│           ├── common.json
│           └── home.json
├── next-i18next.config.js
├── next.config.js
└── pages/

Конфигурация next-i18next

next-i18next.config.js

module.exports = {
  i18n: {
    defaultLocale: 'ru',
    locales: ['ru', 'en'],
  },
};

Интеграция с Next.js

next.config.js

const { i18n } = require('./next-i18next.config');

module.exports = {
  i18n,
};

После этого Next.js начинает автоматически:

  • обрабатывать локали;
  • генерировать маршруты;
  • добавлять locale в router;
  • переключать язык через URL.

Примеры маршрутов:

/
 /ru
 /en

или:

/about
/en/about
/ru/about

Создание файлов переводов

public/locales/ru/common.json

{
  "title": "Главная страница",
  "description": "Описание сайта",
  "welcome": "Добро пожаловать"
}

public/locales/en/common.json

{
  "title": "Home page",
  "description": "Website description",
  "welcome": "Welcome"
}

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

pages/_app.js

import { appWithTranslation } from 'next-i18next';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default appWithTranslation(MyApp);

appWithTranslation:

  • подключает i18next provider;
  • синхронизирует SSR и клиент;
  • передаёт локали в React-контекст.

Использование useTranslation

pages/index.js

import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';

export default function Home() {
  const { t } = useTranslation('common');

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </div>
  );
}

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

serverSideTranslations

Функция:

serverSideTranslations(locale, namespaces)

выполняет:

  1. загрузку переводов;
  2. инициализацию i18next;
  3. передачу данных клиенту;
  4. сериализацию переводов в HTML.

Пример:

await serverSideTranslations(locale, [
  'common',
  'home',
  'footer',
]);

SSR и жизненный цикл перевода

Процесс рендера выглядит следующим образом:

Request
   ↓
Next.js получает locale
   ↓
serverSideTranslations загружает JSON
   ↓
i18next инициализируется
   ↓
React рендерится на сервере
   ↓
HTML отправляется клиенту
   ↓
Hydration
   ↓
Клиент получает уже готовые переводы

Главное преимущество — отсутствие повторной загрузки текста после гидратации.


Разделение переводов по namespace

Большие приложения нельзя хранить в одном JSON.

Плохой вариант:

common.json

на 5000 строк.

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

common.json
home.json
profile.json
dashboard.json
admin.json

Использование нескольких namespace

const { t } = useTranslation(['common', 'home']);

Вызов:

t('common:welcome')
t('home:heroTitle')

Загрузка namespace на сервере

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

Если namespace не загружен:

  • SSR выдаст ошибку;
  • или перевод вернёт ключ.

getStaticProps и SSG

i18next поддерживает статическую генерацию.

Пример

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

Генерация статических страниц для всех локалей

Next.js автоматически создаёт версии страниц:

/en
/ru

Для каждой локали выполняется отдельный build.


Dynamic Routes и локали

pages/blog/[slug].js

export async function getStaticPaths() {
  return {
    paths: [
      {
        params: { slug: 'post-1' },
        locale: 'ru',
      },
      {
        params: { slug: 'post-1' },
        locale: 'en',
      },
    ],
    fallback: false,
  };
}

Переключение языка

Использование router

import { useRouter } from 'next/router';

export default function LanguageSwitcher() {
  const router = useRouter();

  const changeLanguage = (locale) => {
    router.push(router.pathname, router.asPath, {
      locale,
    });
  };

  return (
    <>
      <button onCl ick={() => changeLanguage('ru')}>
        RU
      </button>

      <button onCl ick={() => changeLanguage('en')}>
        EN
      </button>
    </>
  );
}

Что происходит при смене locale

Next.js:

  1. меняет URL;
  2. выполняет новый SSR;
  3. загружает namespace;
  4. рендерит страницу заново.

Автоматическое определение языка

Встроенная поддержка:

module.exports = {
  i18n: {
    locales: ['ru', 'en'],
    defaultLocale: 'ru',
    localeDetection: true,
  },
};

Next.js анализирует:

  • Accept-Language;
  • cookies;
  • текущий URL.

Отключение localeDetection

Иногда автоматическое определение мешает SEO.

localeDetection: false

Это особенно важно для:

  • мультирегиональных сайтов;
  • SEO-оптимизированных landing page;
  • фиксированных языковых URL.

Работа с fallbackLng

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

module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'ru'],
  },

  fallbackLng: 'en',
};

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

{
  "welcome": "Welcome"
}

то i18next использует fallback.


fallbackLng как объект

fallbackLng: {
  ru: ['en'],
  de: ['en'],
  default: ['en'],
}

Интерполяция

Переводы

{
  "hello": "Привет, {{name}}"
}

Использование

t('hello', {
  name: 'Алексей',
});

Результат:

Привет, Алексей

SSR и интерполяция

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

Это означает:

<h1>{t('hello', { name })}</h1>

уже попадёт в HTML как готовая строка.


HTML внутри переводов

Перевод

{
  "content": "Текст <strong>жирный</strong>"
}

Использование Trans

import { Trans } from 'react-i18next';

<Trans i18nKey="content">
  Текст <strong>жирный</strong>
</Trans>

Почему нельзя использовать dangerouslySetInnerHTML

Небезопасный вариант:

<div
  dangerouslySetInnerHTML={{
    __html: t('content'),
  }}
/>

Проблемы:

  • XSS;
  • небезопасный HTML;
  • отсутствие контроля React.

Trans безопаснее и интегрирован с React.


Lazy Loading переводов

Для больших приложений полезна отложенная загрузка namespace.

Пример

const { t } = useTranslation('dashboard');

Namespace загрузится только при открытии страницы.


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

На production:

  • JSON-файлы кэшируются CDN;
  • SSR может использовать memory cache;
  • Next.js кеширует static pages.

Использование CDN

Переводы можно хранить:

  • в CDN;
  • в headless CMS;
  • в locize;
  • во внешнем API.

Подключение HTTP backend

npm install i18next-http-backend

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

import i18n from 'i18next';
import Backend from 'i18next-http-backend';

i18n.use(Backend).init({
  backend: {
    loadPath: '/locales/{{lng}}/{{ns}}.json',
  },
});

Проблема hydration mismatch

Классическая ошибка:

Text content does not match server-rendered HTML

Причины:

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

Как избежать hydration mismatch

Правильная загрузка locale

await serverSideTranslations(locale, ['common'])

Использование одинакового языка

Нельзя:

i18n.changeLanguage(...)

сразу после hydration.


Проблемы browser language detector

Если браузерный detector меняет язык после SSR:

i18n.use(LanguageDetector)

то возможен конфликт.

В SSR-приложениях detector используют осторожно.


Оптимальная схема для SSR

Сервер должен быть главным источником locale.

Правильная последовательность:

URL → locale → SSR → hydration

а не:

Browser language → client rerender

SEO и локализованные страницы

Каждая локаль должна иметь:

  • собственный URL;
  • уникальный HTML;
  • собственные meta tags;
  • hreflang.

hreflang

Пример

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

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

Локализация meta tags

Пример с next/head

import Head from 'next/head';

<Head>
  <title>{t('title')}</title>

  <meta
    name="description"
    content={t('description')}
  />
</Head>

SSR сгенерирует локализованные meta-теги.


Middleware и locale routing

В новых версиях Next.js можно использовать middleware.

middleware.js

import { NextResponse } from 'next/server';

export function middleware(request) {
  const pathname = request.nextUrl.pathname;

  if (pathname === '/') {
    return NextResponse.redirect(
      new URL('/ru', request.url)
    );
  }
}

App Router и i18next

В Next.js App Router интеграция отличается.

Обычно используют:

  • чистый i18next;
  • react-i18next;
  • кастомную серверную инициализацию.

next-i18next исторически ориентирован на Pages Router.


Структура App Router

app/
├── [lng]/
│   ├── layout.js
│   ├── page.js
│   └── about/

Генерация locale routes

generateStaticParams

export async function generateStaticParams() {
  return [
    { lng: 'ru' },
    { lng: 'en' },
  ];
}

Серверная инициализация i18next

lib/i18n.js

import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';

export async function initI18next(locale, ns) {
  await i18next
    .use(initReactI18next)
    .init({
      lng: locale,
      fallbackLng: 'en',

      resources: {
        ru: {
          common: require(
            '../locales/ru/common.json'
          ),
        },

        en: {
          common: require(
            '../locales/en/common.json'
          ),
        },
      },

      ns,
    });

  return i18next;
}

Server Components и переводы

В App Router серверные компоненты могут получать переводы напрямую:

const i18n = await initI18next('ru', ['common']);

const t = i18n.getFixedT('ru', 'common');

Преимущество Server Components

Переводы:

  • не отправляются лишний раз клиенту;
  • уменьшают bundle size;
  • ускоряют hydration.

Client Components

Для клиентских компонентов:

'use client';

используется:

useTranslation()

Разделение Server и Client i18n

Частая архитектура:

server/
client/
shared/

где:

  • сервер отвечает за SSR;
  • клиент — за интерактивность;
  • shared — за namespace и конфиги.

Edge Runtime и i18next

При использовании Edge Runtime важно учитывать:

  • ограничения Node.js API;
  • невозможность использовать fs;
  • необходимость fetch-based загрузки переводов.

Загрузка переводов через fetch

const res = await fetch(
  `https://cdn.site.com/locales/${lng}/${ns}.json`
);

const translations = await res.json();

Локализация ошибок

pages/404.js

export default function NotFound() {
  const { t } = useTranslation('common');

  return <h1>{t('notFound')}</h1>;
}

Локализация API ошибок

Пример:

return res.status(400).json({
  message: t('errors.invalidEmail'),
});

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

i18next не заменяет Intl API.

Для SSR рекомендуется:

new Intl.NumberFormat(locale)
new Intl.DateTimeFormat(locale)

Пример NumberFormat

new Intl.NumberFormat('ru-RU').format(1000000)

Результат:

1 000 000

Пример DateTimeFormat

new Intl.DateTimeFormat('ru-RU').format(
  new Date()
)

ICU и pluralization

Для сложной локализации используют ICU.

Установка

npm install i18next-icu

Пример pluralization

{
  "items": "{{count}} item",
  "items_plural": "{{count}} items"
}

Использование

t('items', {
  count: 10,
});

Русские множественные формы

Русский язык сложнее английского.

i18next поддерживает:

  • one;
  • few;
  • many;
  • other.

Пример

{
  "cart_one": "{{count}} товар",
  "cart_few": "{{count}} товара",
  "cart_many": "{{count}} товаров"
}

Производительность SSR

Основные проблемы:

  • большое количество namespace;
  • тяжёлые JSON;
  • повторная инициализация i18next;
  • отсутствие кэширования.

Оптимизация

Минимизировать namespace

Плохо:

['common', 'home', 'admin', 'dashboard']

на каждой странице.

Лучше:

['home']

только для нужного route.


Кэш инстансов

На сервере можно переиспользовать i18next instance.


Изоляция запросов

Нельзя хранить mutable locale глобально:

i18n.language = locale

Это вызывает race conditions при SSR.


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

Создавать отдельный instance:

createInstance()

Пример createInstance

import { createInstance } from 'i18next';

const instance = createInstance();

Debug режим

debug: true

Позволяет видеть:

  • загрузку namespace;
  • missing keys;
  • fallback;
  • lifecycle i18next.

saveMissing

Полезно при разработке.

saveMissing: true

Отсутствующие ключи автоматически логируются.


Обработка missing key

missingKeyHandler(lng, ns, key) {
  console.log(key);
}

Типизация переводов в TypeScript

Можно типизировать namespace.

Пример

type TranslationKeys =
  | 'title'
  | 'description'
  | 'welcome';

Генерация типов

Популярный подход:

  • парсинг JSON;
  • генерация union type;
  • автокомплит ключей.

Тестирование локализации

Проверяют:

  • SSR;
  • fallback;
  • namespace loading;
  • hydration;
  • pluralization;
  • routing.

Тест SSR

expect(html).toContain('Добро пожаловать');

E2E тестирование

Обычно используют:

  • Playwright
  • Cypress

Проверка локалей

Тесты обычно проверяют:

/ru
/en

и корректность:

  • title;
  • SEO;
  • meta tags;
  • текста;
  • redirect logic.

Распространённые ошибки

Глобальный singleton i18next

Вызывает проблемы при SSR.


Слишком большой common.json

Ухудшает:

  • TTFB;
  • hydration;
  • memory usage.

Client-side language switch после SSR

Приводит к hydration mismatch.


Отсутствие namespace preload

Результат:

missingKey

на сервере.


Использование fs в Edge Runtime

В Edge Runtime файловой системы нет.


Рекомендуемая архитектура

Pages Router

  • next-i18next;
  • namespace splitting;
  • serverSideTranslations;
  • locale routing через Next.js.

App Router

  • отдельная server/client инициализация;
  • createInstance;
  • fetch-based translation loading;
  • Server Components для SSR-переводов.