Несколько языков: обзор
Интернационализация (i18n) — подготовка сайта к работе на нескольких языках. В App Router нет встроенного переключения языков: его собирают из уже знакомых частей — динамического сегмента, файлов с переводами и прокси (proxy.ts). Здесь показана общая схема.
Язык в адресе
Язык делают первым сегментом адреса: /ru/posts, /en/posts. Для этого все страницы переносят в папку app/[lang]:
app/
└── [lang]/
├── layout.tsx
├── page.tsx /ru, /en
└── posts/
└── page.tsx /ru/posts, /en/postsТеперь каждая страница получает lang в params.
Словари
Переводы хранят в JSON-файлах, по одному на язык:
{
"posts": "Статьи",
"newPost": "Новая статья"
}Это файл dictionaries/ru.json; в dictionaries/en.json те же ключи с английским текстом. Функция загрузки словаря:
// lib/dictionaries.ts
import 'server-only';
const dictionaries = {
ru: () => import('@/dictionaries/ru.json').then((module) => module.default),
en: () => import('@/dictionaries/en.json').then((module) => module.default),
};
export type Locale = keyof typeof dictionaries;
export async function getDictionary(locale: Locale) {
return dictionaries[locale]();
}Пакет server-only (устанавливается командой npm install server-only) гарантирует, что файл не попадёт в браузер.
Перевод на странице
// app/[lang]/posts/page.tsx
import { getDictionary, type Locale } from '@/lib/dictionaries';
export default async function PostsPage({ params }: { params: Promise<{ lang: Locale }> }) {
const { lang } = await params;
const t = await getDictionary(lang);
return <h1>{t.posts}</h1>;
}/ru/posts покажет «Статьи», /en/posts — «Posts». Атрибут lang тега html в макете app/[lang]/layout.tsx тоже берут из params.
Выбор языка по умолчанию
Адрес /posts без языка обрабатывает прокси: читает заголовок accept-language, выбирает подходящий язык и перенаправляет, например, на /ru/posts. Если язык браузера не поддерживается, берут язык по умолчанию.
Готовые библиотеки
Ручная схема годится для пары языков и простых строк. Для форм множественного числа («1 статья», «5 статей»), дат, чисел и переключателя языка удобнее библиотека next-intl — она построена на тех же принципах, но многое делает за вас.
Частая ошибка
Забыть проверить значение lang. Адрес /de/posts тоже попадёт в папку [lang], и dictionaries['de'] окажется undefined — страница упадёт с ошибкой. Проверяйте язык по списку поддерживаемых и вызывайте notFound(), если его там нет.