При серверном рендеринге приложение должно возвращать уже локализованный HTML. Это влияет на:
Связка Next.js + 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/
module.exports = {
i18n: {
defaultLocale: 'ru',
locales: ['ru', 'en'],
},
};
const { i18n } = require('./next-i18next.config');
module.exports = {
i18n,
};
После этого Next.js начинает автоматически:
Примеры маршрутов:
/
/ru
/en
или:
/about
/en/about
/ru/about
{
"title": "Главная страница",
"description": "Описание сайта",
"welcome": "Добро пожаловать"
}
{
"title": "Home page",
"description": "Website description",
"welcome": "Welcome"
}
import { appWithTranslation } from 'next-i18next';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default appWithTranslation(MyApp);
appWithTranslation:
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(locale, namespaces)
выполняет:
Пример:
await serverSideTranslations(locale, [
'common',
'home',
'footer',
]);
Процесс рендера выглядит следующим образом:
Request
↓
Next.js получает locale
↓
serverSideTranslations загружает JSON
↓
i18next инициализируется
↓
React рендерится на сервере
↓
HTML отправляется клиенту
↓
Hydration
↓
Клиент получает уже готовые переводы
Главное преимущество — отсутствие повторной загрузки текста после гидратации.
Большие приложения нельзя хранить в одном JSON.
Плохой вариант:
common.json
на 5000 строк.
Правильный подход:
common.json
home.json
profile.json
dashboard.json
admin.json
const { t } = useTranslation(['common', 'home']);
Вызов:
t('common:welcome')
t('home:heroTitle')
export async function getServerSideProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, [
'common',
'home',
])),
},
};
}
Если namespace не загружен:
i18next поддерживает статическую генерацию.
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, [
'common',
])),
},
};
}
Next.js автоматически создаёт версии страниц:
/en
/ru
Для каждой локали выполняется отдельный build.
export async function getStaticPaths() {
return {
paths: [
{
params: { slug: 'post-1' },
locale: 'ru',
},
{
params: { slug: 'post-1' },
locale: 'en',
},
],
fallback: false,
};
}
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>
</>
);
}
Next.js:
Встроенная поддержка:
module.exports = {
i18n: {
locales: ['ru', 'en'],
defaultLocale: 'ru',
localeDetection: true,
},
};
Next.js анализирует:
Accept-Language;Иногда автоматическое определение мешает SEO.
localeDetection: false
Это особенно важно для:
Конфигурация:
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'ru'],
},
fallbackLng: 'en',
};
Если перевода нет:
{
"welcome": "Welcome"
}
то i18next использует fallback.
fallbackLng: {
ru: ['en'],
de: ['en'],
default: ['en'],
}
{
"hello": "Привет, {{name}}"
}
t('hello', {
name: 'Алексей',
});
Результат:
Привет, Алексей
При SSR интерполяция выполняется на сервере.
Это означает:
<h1>{t('hello', { name })}</h1>
уже попадёт в HTML как готовая строка.
{
"content": "Текст <strong>жирный</strong>"
}
import { Trans } from 'react-i18next';
<Trans i18nKey="content">
Текст <strong>жирный</strong>
</Trans>
Небезопасный вариант:
<div
dangerouslySetInnerHTML={{
__html: t('content'),
}}
/>
Проблемы:
Trans безопаснее и интегрирован с React.
Для больших приложений полезна отложенная загрузка namespace.
const { t } = useTranslation('dashboard');
Namespace загрузится только при открытии страницы.
На production:
Переводы можно хранить:
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',
},
});
Классическая ошибка:
Text content does not match server-rendered HTML
Причины:
await serverSideTranslations(locale, ['common'])
Нельзя:
i18n.changeLanguage(...)
сразу после hydration.
Если браузерный detector меняет язык после SSR:
i18n.use(LanguageDetector)
то возможен конфликт.
В SSR-приложениях detector используют осторожно.
Сервер должен быть главным источником locale.
Правильная последовательность:
URL → locale → SSR → hydration
а не:
Browser language → client rerender
Каждая локаль должна иметь:
<link
rel="alternate"
hreflang="en"
href="https://site.com/en"
/>
<link
rel="alternate"
hreflang="ru"
href="https://site.com/ru"
/>
import Head from 'next/head';
<Head>
<title>{t('title')}</title>
<meta
name="description"
content={t('description')}
/>
</Head>
SSR сгенерирует локализованные meta-теги.
В новых версиях Next.js можно использовать middleware.
import { NextResponse } from 'next/server';
export function middleware(request) {
const pathname = request.nextUrl.pathname;
if (pathname === '/') {
return NextResponse.redirect(
new URL('/ru', request.url)
);
}
}
В Next.js App Router интеграция отличается.
Обычно используют:
next-i18next исторически ориентирован на Pages
Router.
app/
├── [lng]/
│ ├── layout.js
│ ├── page.js
│ └── about/
export async function generateStaticParams() {
return [
{ lng: 'ru' },
{ lng: 'en' },
];
}
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;
}
В App Router серверные компоненты могут получать переводы напрямую:
const i18n = await initI18next('ru', ['common']);
const t = i18n.getFixedT('ru', 'common');
Переводы:
Для клиентских компонентов:
'use client';
используется:
useTranslation()
Частая архитектура:
server/
client/
shared/
где:
При использовании Edge Runtime важно учитывать:
const res = await fetch(
`https://cdn.site.com/locales/${lng}/${ns}.json`
);
const translations = await res.json();
export default function NotFound() {
const { t } = useTranslation('common');
return <h1>{t('notFound')}</h1>;
}
Пример:
return res.status(400).json({
message: t('errors.invalidEmail'),
});
i18next не заменяет Intl API.
Для SSR рекомендуется:
new Intl.NumberFormat(locale)
new Intl.DateTimeFormat(locale)
new Intl.NumberFormat('ru-RU').format(1000000)
Результат:
1 000 000
new Intl.DateTimeFormat('ru-RU').format(
new Date()
)
Для сложной локализации используют ICU.
npm install i18next-icu
{
"items": "{{count}} item",
"items_plural": "{{count}} items"
}
t('items', {
count: 10,
});
Русский язык сложнее английского.
i18next поддерживает:
{
"cart_one": "{{count}} товар",
"cart_few": "{{count}} товара",
"cart_many": "{{count}} товаров"
}
Основные проблемы:
Плохо:
['common', 'home', 'admin', 'dashboard']
на каждой странице.
Лучше:
['home']
только для нужного route.
На сервере можно переиспользовать i18next instance.
Нельзя хранить mutable locale глобально:
i18n.language = locale
Это вызывает race conditions при SSR.
Создавать отдельный instance:
createInstance()
import { createInstance } from 'i18next';
const instance = createInstance();
debug: true
Позволяет видеть:
Полезно при разработке.
saveMissing: true
Отсутствующие ключи автоматически логируются.
missingKeyHandler(lng, ns, key) {
console.log(key);
}
Можно типизировать namespace.
type TranslationKeys =
| 'title'
| 'description'
| 'welcome';
Популярный подход:
Проверяют:
expect(html).toContain('Добро пожаловать');
Обычно используют:
Тесты обычно проверяют:
/ru
/en
и корректность:
Вызывает проблемы при SSR.
Ухудшает:
Приводит к hydration mismatch.
Результат:
missingKey
на сервере.
В Edge Runtime файловой системы нет.