Получайте уведомления о предстоящих релизах Intlayer
    Создание:2025-11-01Последнее обновление:2025-11-01

    Как интернационализировать ваше приложение Next.js с помощью next-i18next в 2025 году

    Содержание

    Что такое next-i18next?

    next-i18next — это популярное решение для интернационализации (i18n) приложений Next.js. В то время как оригинальный пакет next-i18next был разработан для Pages Router, в этом руководстве показано, как реализовать i18next с современным App Router, используя напрямую i18next и react-i18next.

    С таким подходом вы можете:

    • Организовывать переводы с помощью пространств имён (например, common.json, about.json) для лучшего управления контентом.
    • Эффективно загружать переводы, загружая только необходимые пространства имён для каждой страницы, что уменьшает размер бандла.
    • Поддерживать как серверные, так и клиентские компоненты с правильной обработкой SSR и гидратации.
    • Обеспечить поддержку TypeScript с типобезопасной конфигурацией локалей и ключей переводов.
    • Оптимизируйте SEO с помощью правильных метаданных, карты сайта и интернационализации robots.txt.
    В качестве альтернативы вы также можете ознакомиться с руководством next-intl или использовать напрямую Intlayer.
    Сравнение смотрите в next-i18next vs next-intl vs Intlayer.

    Практики, которые следует соблюдать

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

    • Устанавливайте атрибуты HTML lang и dir В вашем layout вычисляйте dir с помощью getLocaleDirection(locale) и устанавливайте <html lang={locale} dir={dir}> для правильной доступности и SEO.
    • Разделяйте сообщения по namespace Организуйте JSON-файлы по локалям и namespace (например, common.json, about.json), чтобы загружать только необходимое.
    • Минимизируйте нагрузку на клиент На страницах отправляйте в NextIntlClientProvider только нужные namespace (например, pick(messages, ['common', 'about'])).
    • Предпочитайте статические страницы Используйте статические страницы по возможности для лучшей производительности и SEO.
    • Интернационализация в серверных компонентах Серверные компоненты, такие как страницы или все компоненты, не помеченные как client, являются статическими и могут быть предварительно отрендерены во время сборки. Поэтому нам нужно передавать функции перевода им через props.
    • Настройте типы TypeScript Для ваших локалей обеспечьте типовую безопасность по всему приложению.
    • Прокси для перенаправления Используйте прокси для обработки определения локали и маршрутизации, а также для перенаправления пользователя на URL с соответствующим префиксом локали.
    • Интернационализация метаданных, sitemap, robots.txt Интернационализируйте ваши метаданные, sitemap, robots.txt с помощью функции generateMetadata, предоставляемой Next.js, чтобы обеспечить лучшее обнаружение поисковыми системами во всех локалях.
    • Локализация ссылок Локализуйте ссылки с помощью компонента Link, чтобы перенаправлять пользователя на URL с соответствующим префиксом локали. Это важно для обеспечения обнаружения ваших страниц во всех локалях.
    • Автоматизация тестов и переводов Автоматизация тестов и переводов помогает экономить время на поддержку вашего многоязычного приложения.
    Смотрите нашу документацию, в которой перечислено все, что вам нужно знать об интернационализации и SEO: Интернационализация (i18n) с next-intl.

    Пошаговое руководство по настройке i18next в приложении Next.js

    Смотрите шаблон приложения на GitHub.

    Вот структура проекта, которую мы будем создавать:

    .├── i18n.config.ts└── src # Src необязателен    ├── locales    │   ├── en    │   │  ├── common.json    │   │  └── about.json    │   └── fr    │      ├── common.json    │      └── about.json    ├── types    │   └── i18next.d.ts    ├── app    │   ├── proxy.ts    │   ├── i18n    │   │   └── server.ts    │   └── [locale]    │       ├── layout.tsx    │       ├── (home) # / (Группа маршрутов, чтобы не засорять все страницы сообщениями домашней страницы)    │       │   ├── layout.tsx    │       │   └── page.tsx    │       └── about # /about    │           ├── layout.tsx    │           └── page.tsx    └── components        ├── I18nProvider.tsx        ├── ClientComponent.tsx        └── ServerComponent.tsx

    Шаг 1: Установка зависимостей

    Установите необходимые пакеты с помощью npm:

    npm install i18next react-i18next i18next-resources-to-backend
    • i18next: Основной фреймворк интернационализации, который отвечает за загрузку и управление переводами.
    • react-i18next: Обертки для React над i18next, предоставляющие хуки, такие как useTranslation, для клиентских компонентов.
    • i18next-resources-to-backend: Плагин, который позволяет динамически загружать файлы переводов, загружая только необходимые пространства имён.

    Шаг 2: Настройка проекта

    Создайте файл конфигурации, чтобы определить поддерживаемые локали, локаль по умолчанию и вспомогательные функции для локализации URL. Этот файл служит единственным источником правды для вашей настройки i18n и обеспечивает типобезопасность во всем приложении.

    Централизация конфигурации локалей предотвращает несоответствия и упрощает добавление или удаление локалей в будущем. Вспомогательные функции обеспечивают единообразное формирование URL для SEO и маршрутизации.

    i18n.config.ts
    // Определяем поддерживаемые локали как константный массив для типобезопасности// Утверждение 'as const' заставляет TypeScript выводить литеральные типы вместо string[]export const locales = ["en", "fr"] as const;// Извлекаем тип Locale из массива локалей// Это создаёт объединённый тип: "en" | "fr"export type Locale = (typeof locales)[number];// Установите локаль по умолчанию, используемую, когда локаль не указанаexport const defaultLocale: Locale = "en";// Языки с направлением текста справа налево, требующие специальной обработкиexport const rtlLocales = ["ar", "he", "fa", "ur"] as const;// Проверяет, требует ли локаль направление текста справа налево (RTL)// Используется для таких языков, как арабский, иврит, персидский и урдуexport const isRtl = (locale: string) =>  (rtlLocales as readonly string[]).includes(locale);// Генерирует локализованный путь для заданной локали и пути// Пути для локали по умолчанию не имеют префикса (например, "/about" вместо "/en/about")// Для других локалей добавляется префикс (например, "/fr/about")export function localizedPath(locale: string, path: string) {  return locale === defaultLocale ? path : `/${locale}${path}`;}// Базовый URL для абсолютных URL (используется в sitemap, метаданных и т.д.)const ORIGIN = "https://example.com";// Генерация абсолютного URL с префиксом локали// Используется для SEO метаданных, sitemap и канонических URLexport function absoluteUrl(locale: string, path: string) {  return `${ORIGIN}${localizedPath(locale, path)}`;}// Используется для установки cookie локали в браузереexport function getCookie(locale: Locale) {  return [    `NEXT_LOCALE=${locale}`,    "Path=/",    `Max-Age=${60 * 60 * 24 * 365}`, // 1 год    "SameSite=Lax",  ].join("; ");}

    Шаг 3: Централизовать пространства имён переводов

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

    src/i18n.namespaces.ts
    export const namespaces = ["common", "about"] as const;export type Namespace = (typeof namespaces)[number];

    Шаг 4: Строгая типизация ключей перевода с помощью TypeScript

    Расширьте i18next, чтобы он ссылался на ваши канонические языковые файлы (обычно английские). TypeScript тогда выводит допустимые ключи для каждого namespace, поэтому вызовы t() проверяются полностью.

    src/types/i18next.d.ts
    import "i18next";declare module "i18next" {  interface CustomTypeOptions {    defaultNS: "common";    resources: {      common: typeof import("@/locales/en/common.json");      about: typeof import("@/locales/en/about.json");    };  }}
    Совет: Сохраните это объявление в папке src/types (создайте папку, если её нет). Next.js уже включает src в tsconfig.json, поэтому расширение будет автоматически подхвачено. Если нет, добавьте следующее в ваш файл tsconfig.json:
    tsconfig.json
    {  "include": ["src/types/**/*.ts"],}

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

    import { useTranslation, type TFunction } from "react-i18next";const { t } = useTranslation("about");// ОК, типизировано: t("counter.increment")// ОШИБКА, ошибка компиляции: t("doesNotExist")export type AboutTranslator = TFunction<"about">;

    Шаг 5: Настройка инициализации i18n на стороне сервера

    Создайте функцию инициализации на стороне сервера, которая загружает переводы для серверных компонентов. Эта функция создает отдельный экземпляр i18next для серверного рендеринга, обеспечивая загрузку переводов до рендеринга.

    Серверным компонентам нужен собственный экземпляр i18next, так как они работают в другом контексте, чем клиентские компоненты. Предварительная загрузка переводов на сервере предотвращает появление непереведенного контента и улучшает SEO, гарантируя, что поисковые системы видят переведенный контент.

    src/app/i18n/server.ts
    import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";import { namespaces, type Namespace } from "@/i18n.namespaces";// Настройка динамической загрузки ресурсов для i18next// Эта функция динамически импортирует JSON-файлы переводов на основе локали и пространства имён// Пример: locale="fr", namespace="about" -> импортирует "@/locales/fr/about.json"const backend = resourcesToBackend(  (locale: string, namespace: string) =>    import(`@/locales/${locale}/${namespace}.json`));const DEFAULT_NAMESPACES = [  namespaces[0],] as const satisfies readonly Namespace[];/** * Инициализация экземпляра i18next для серверного рендеринга * * @returns Инициализированный экземпляр i18next, готовый к использованию на сервере */export async function initI18next(  locale: string,  ns: readonly Namespace[] = DEFAULT_NAMESPACES) {  // Создайте новый экземпляр i18next (отдельный от клиентского экземпляра)  const i18n = createInstance();  // Инициализация с интеграцией React и загрузчиком backend  await i18n    .use(initReactI18next) // Включить поддержку React hooks    .use(backend) // Включить динамическую загрузку ресурсов    .init({      lng: locale,      fallbackLng: defaultLocale,      ns, // Загружать только указанные пространства имён для лучшей производительности      defaultNS: "common", // Пространство имён по умолчанию, если не указано другое      interpolation: { escapeValue: false }, // Не экранировать HTML (React обеспечивает защиту от XSS)      react: { useSuspense: false }, // Отключить Suspense для совместимости с SSR      returnNull: false, // Возвращать пустую строку вместо null для отсутствующих ключей      initImmediate: false, // Отложить инициализацию до загрузки ресурсов (ускоряет SSR)    });  return i18n;}

    Шаг 6: Создание клиентского провайдера i18n

    Создайте клиентский компонент-провайдер, который оборачивает ваше приложение в контекст i18next. Этот провайдер получает предварительно загруженные переводы с сервера, чтобы предотвратить мерцание непереведённого контента (FOUC) и избежать повторных запросов.

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

    src/components/I18nProvider.tsx
    "use client";import { useEffect, useState } from "react";import { I18nextProvider } from "react-i18next";import { createInstance, type ResourceLanguage } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";import { namespaces as allNamespaces, type Namespace } from "@/i18n.namespaces";// Настройка динамической загрузки ресурсов для клиентской стороны// Такой же паттерн, как и на сервере, но этот экземпляр работает в браузереconst backend = resourcesToBackend(  (locale: string, namespace: string) =>    import(`@/locales/${locale}/${namespace}.json`));type Props = {  locale: string;  namespaces?: readonly Namespace[];  // Предзагруженные ресурсы с сервера (предотвращает FOUC - мерцание непереведённого контента)  // Формат: { namespace: translationBundle }  resources?: Record<Namespace, ResourceLanguage>;  children: React.ReactNode;};/** * Провайдер i18n на стороне клиента, который оборачивает приложение в контекст i18next * Получает предварительно загруженные ресурсы с сервера, чтобы избежать повторного запроса переводов */export default function I18nProvider({  locale,  namespaces = [allNamespaces[0]] as const,  resources,  children,}: Props) {  // Создаем экземпляр i18n один раз с помощью ленивой инициализации useState  // Это гарантирует, что экземпляр создается только один раз, а не при каждом рендере  const [i18n] = useState(() => {    const i18nInstance = createInstance();    i18nInstance      .use(initReactI18next)      .use(backend)      .init({        lng: locale,        fallbackLng: defaultLocale,        ns: namespaces,        // Если ресурсы предоставлены (с сервера), используйте их, чтобы избежать загрузки переводов на клиенте        // Это предотвращает FOUC и улучшает производительность при первоначальной загрузке        resources: resources ? { [locale]: resources } : undefined,        defaultNS: "common",        interpolation: { escapeValue: false },        react: { useSuspense: false },        returnNull: false, // Предотвращает возврат undefined значений      });    return i18nInstance;  });  // Обновление языка при изменении свойства locale  useEffect(() => {    i18n.changeLanguage(locale);  }, [locale, i18n]);  // Обеспечить загрузку всех необходимых пространств имён на клиенте  // Использование join("|") в качестве зависимости для корректного сравнения массивов  useEffect(() => {    i18n.loadNamespaces(namespaces);  }, [namespaces.join("|"), i18n]);  // Предоставить экземпляр i18n всем дочерним компонентам через React context  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;}

    Шаг 7: Определение динамических маршрутов локалей

    Настройте динамическую маршрутизацию для локалей, создав директорию [locale] в вашей папке app. Это позволяет Next.js обрабатывать маршрутизацию на основе локалей, где каждая локаль становится сегментом URL (например, /en/about, /fr/about).

    Использование динамических маршрутов позволяет Next.js генерировать статические страницы для всех локалей во время сборки, что улучшает производительность и SEO. Компонент layout устанавливает атрибуты HTML lang и dir в зависимости от локали, что важно для доступности и понимания поисковыми системами.

    src/app/[locale]/layout.tsx
    import type { ReactNode } from "react";import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";// Отключить динамические параметры - все локали должны быть известны на этапе сборки// Это обеспечивает статическую генерацию для всех маршрутов локалейexport const dynamicParams = false;/** * Генерировать статические параметры для всех локалей на этапе сборки * Next.js предварительно отрендерит страницы для каждой локали, возвращенной здесь * Пример: [{ locale: "en" }, { locale: "fr" }] */export function generateStaticParams() {  return locales.map((locale) => ({ locale }));}/** * Корневой компонент layout, который обрабатывает HTML-атрибуты, специфичные для локали * Устанавливает атрибут lang и направление текста (ltr/rtl) в зависимости от локали */export default function LocaleLayout({  children,  params,}: {  children: ReactNode;  params: { locale: string };}) {  // Проверка локали из параметров URL  // Если указана недопустимая локаль, используется локаль по умолчанию  const locale: Locale = (locales as readonly string[]).includes(params.locale)    ? (params.locale as any)    : defaultLocale;  // Определение направления текста на основе локали  // Языки с письмом справа налево, такие как арабский, требуют dir="rtl" для правильного отображения текста  const dir = isRtl(locale) ? "rtl" : "ltr";  return (    <html lang={locale} dir={dir}>      <body>{children}</body>    </html>  );}

    Шаг 8: Создайте файлы переводов

    Создайте JSON-файлы для каждой локали и пространства имён. Такая структура позволяет логично организовать переводы и загружать только то, что необходимо для каждой страницы.

    Организация переводов по пространствам имён (например, common.json, about.json) позволяет разделять код и уменьшать размер бандла. Вы загружаете только те переводы, которые нужны для каждой страницы, что улучшает производительность.

    src/locales/en/common.json
    {  "appTitle": "Next.js i18n App",  "appDescription": "Example Next.js application with internationalization using i18next"}
    src/locales/fr/common.json
    {  "appTitle": "Application Next.js i18n",  "appDescription": "Exemple d'application Next.js avec internationalisation utilisant i18next"}
    src/locales/en/home.json
    {  "title": "Home",  "description": "Home page description",  "welcome": "Welcome",  "greeting": "Hello, world!",  "aboutPage": "About Page",  "documentation": "Documentation"}

    Организация переводов по пространствам имён (например, common.json, about.json) позволяет разделять код и уменьшать размер бандла. Вы загружаете только те переводы, которые нужны для каждой страницы, что улучшает производительность.

    src/locales/en/common.json
    {  "appTitle": "Next.js i18n App",  "appDescription": "Example Next.js application with internationalization using i18next"}
    src/locales/ru/common.json
    {  "appTitle": "Приложение Next.js с i18n",  "appDescription": "Пример приложения Next.js с интернационализацией с использованием i18next"}
    src/locales/en/home.json
    {  "title": "Home",  "description": "Home page description",  "welcome": "Welcome",  "greeting": "Hello, world!",  "aboutPage": "About Page",  "documentation": "Documentation"}
    src/locales/ru/home.json
    {  "title": "Главная",  "description": "Описание главной страницы",  "welcome": "Добро пожаловать",  "greeting": "Привет, мир!",  "aboutPage": "Страница О нас",  "documentation": "Документация"}
    src/locales/en/about.json
    {  "title": "About",  "description": "About page description",  "counter": {    "label": "Counter",    "increment": "Increment",    "description": "Click the button to increase the counter"  }}
    src/locales/ru/about.json
    {  "title": "О нас",  "description": "Описание страницы О нас",  "counter": {    "label": "Счётчик",    "increment": "Увеличить",    "description": "Нажмите кнопку, чтобы увеличить счётчик"  }}

    Шаг 9: Использование переводов на ваших страницах

    Создайте компонент страницы, который инициализирует i18next на сервере и передаёт переводы как серверным, так и клиентским компонентам. Это гарантирует, что переводы загружены до рендера и предотвращает мерцание контента.

    Инициализация на стороне сервера загружает переводы до рендера страницы, улучшая SEO и предотвращая FOUC (Flash of Unstyled Content). Передавая заранее загруженные ресурсы клиентскому провайдеру, мы избегаем повторных запросов и обеспечиваем плавную гидратацию.

    src/app/[locale]/about/index.tsx
    import I18nProvider from "@/components/I18nProvider";import { initI18next } from "@/app/i18n/server";import type { Locale } from "@/i18n.config";import { namespaces as allNamespaces, type Namespace } from "@/i18n.namespaces";import type { ResourceLanguage } from "i18next";import ClientComponent from "@/components/ClientComponent";import ServerComponent from "@/components/ServerComponent";/** * Серверный компонент страницы, который инициализирует i18n * Предварительно загружает переводы на сервере и передает их клиентским компонентам */export default async function AboutPage({  params: { locale },}: {  params: { locale: Locale };}) {  // Определяем, какие пространства имен переводов нужны этой странице  // Используем централизованный список для типобезопасности и автодополнения  const pageNamespaces = allNamespaces;  // Инициализируем i18next на сервере с необходимыми пространствами имен  // Это загружает JSON-файлы переводов на сервере  const i18n = await initI18next(locale, pageNamespaces);  // Получить фиксированную функцию перевода для пространства имён "about"  // getFixedT фиксирует пространство имён, поэтому используется t("title") вместо t("about:title")  const tAbout = i18n.getFixedT(locale, "about");  // Извлечь пакеты переводов из экземпляра i18n  // Эти данные передаются в I18nProvider для гидратации i18n на стороне клиента  // Предотвращает FOUC (вспышку непереведённого контента) и избегает повторного запроса  const resources = Object.fromEntries(    pageNamespaces.map((ns) => [ns, i18n.getResourceBundle(locale, ns)])  ) satisfies Record<Namespace, ResourceLanguage>;  return (    <I18nProvider      locale={locale}      namespaces={pageNamespaces}      resources={resources}    >      <main>        <h1>{tAbout("title")}</h1>        <ClientComponent />        <ServerComponent t={tAbout} locale={locale} count={0} />      </main>    </I18nProvider>  );}

    Шаг 10: Использование переводов в клиентских компонентах

    Клиентские компоненты могут использовать хук useTranslation для доступа к переводам. Этот хук предоставляет функцию перевода и экземпляр i18n, что позволяет переводить контент и получать информацию о локали.

    Клиентским компонентам необходимы React хуки для доступа к переводам. Хук useTranslation интегрируется с i18next и обеспечивает реактивное обновление при изменении локали.

    Убедитесь, что страница/провайдер включает только необходимые пространства имён (например, about).
    Если вы используете React < 19, мемоизируйте тяжёлые форматтеры, такие как Intl.NumberFormat.

    src/components/ClientComponent.tsx
    "use client";import { useState } from "react";import { useTranslation } from "react-i18next";/** * Пример клиентского компонента с использованием React hooks для переводов * Можно использовать хуки, такие как useState, useEffect и useTranslation */const ClientComponent = () => {  // Хук useTranslation предоставляет доступ к функции перевода и экземпляру i18n  // Указываем namespace, чтобы загружать только переводы для пространства "about"  const { t, i18n } = useTranslation("about");  const [count, setCount] = useState(0);  // Создаем форматтер чисел с учетом локали  // i18n.language предоставляет текущую локаль (например, "en", "fr")  // Intl.NumberFormat форматирует числа согласно локальным стандартам  const numberFormat = new Intl.NumberFormat(i18n.language);  return (    <div className="flex flex-col items-center gap-4">      {/* Форматирование числа с учетом локали */}      <p className="text-5xl font-bold text-white m-0">        {numberFormat.format(count)}      </p>      <button        type="button"        className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"        aria-label={t("counter.label")}        onClick={() => setCount((c) => c + 1)}      >        {t("counter.increment")}      </button>    </div>  );};export default ClientComponent;

    Шаг 11: Использование переводов в серверных компонентах

    Серверные компоненты не могут использовать React hooks, поэтому они получают переводы через props от своих родительских компонентов. Такой подход сохраняет синхронность серверных компонентов и позволяет вкладывать их внутрь клиентских компонентов.

    Серверные компоненты, которые могут быть вложены в клиентские границы, должны быть синхронными. Передавая переведённые строки и информацию о локали через props, мы избегаем асинхронных операций и обеспечиваем корректный рендеринг.

    src/components/ServerComponent.tsx
    import type { TFunction } from "i18next";type ServerComponentProps = {  // Функция перевода, переданная от родительского серверного компонента  // Серверные компоненты не могут использовать hooks, поэтому переводы приходят через props  t: TFunction<"about">;  locale: string;  count: number;};/** * Пример серверного компонента - получает переводы через props * Может быть вложен в клиентские компоненты (асинхронные серверные компоненты) * Не может использовать React hooks, поэтому все данные должны поступать через props или асинхронные операции */const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {  // Форматирование числа на сервере с использованием локали  // Выполняется на сервере во время SSR, улучшая начальную загрузку страницы  const formatted = new Intl.NumberFormat(locale).format(count);  return (    <div className="flex flex-col items-center gap-4">      <p className="text-5xl font-bold text-white m-0">{formatted}</p>      {/* Использование функции перевода, переданной через props */}      <div className="flex flex-col items-center gap-2">        <span className="text-xl font-semibold text-white">          {t("counter.label")}        </span>        <span className="text-sm opacity-80 italic">          {t("counter.description")}        </span>      </div>    </div>  );};export default ServerComponent;

    (Необязательно) Шаг 12: Изменение языка вашего контента

    Для изменения языка вашего контента в Next.js рекомендуется использовать URL с префиксом локали и ссылки Next.js. Пример ниже считывает текущую локаль из маршрута, удаляет её из пути и отображает по одной ссылке для каждой доступной локали.

    src/components/LocaleSwitcher.tsx
    "use client";import Link from "next/link";import { useParams, usePathname } from "next/navigation";import { useMemo } from "react";import { defaultLocale, getCookie, type Locale, locales } from "@/i18n.config";export default function LocaleSwitcher() {  const params = useParams();  const pathname = usePathname();  const activeLocale = (params?.locale as Locale | undefined) ?? defaultLocale;  const getLocaleLabel = (locale: Locale): string => {    try {      const displayNames = new Intl.DisplayNames([locale], {        type: "language",      });      return displayNames.of(locale) ?? locale.toUpperCase();    } catch {      return locale.toUpperCase();    }  };  const basePath = useMemo(() => {    if (!pathname) return "/";    const segments = pathname.split("/").filter(Boolean);    if (segments.length === 0) return "/";    const maybeLocale = segments[0] as Locale;    if ((locales as readonly string[]).includes(maybeLocale)) {      const rest = segments.slice(1).join("/");      return rest ? `/${rest}` : "/";    }    return pathname;  }, [pathname]);  return (    <nav aria-label="Выбор языка">      {(locales as readonly Locale[]).map((locale) => {        const isActive = locale === activeLocale;        const href =          locale === defaultLocale ? basePath : `/${locale}${basePath}`;        return (          <Link            key={locale}            href={href}            aria-current={isActive ? "page" : undefined}            onClick={() => {              document.cookie = getCookie(locale);            }}          >            {getLocaleLabel(locale)}          </Link>        );      })}    </nav>  );}

    Повторное использование локализованных URL по всему вашему приложению обеспечивает согласованную навигацию и улучшает SEO. Оберните next/link в небольшой помощник, который добавляет префикс активной локали к внутренним маршрутам, оставляя внешние URL без изменений.

    src/components/LocalizedLink.tsx
    "use client";import NextLink, { type LinkProps } from "next/link";import { useParams } from "next/navigation";import type { ComponentProps, PropsWithChildren } from "react";import {  defaultLocale,  type Locale,  locales,  localizedPath,} from "@/i18n.config";const isExternal = (href: string) => /^https?:\/\//.test(href);type LocalizedLinkProps = PropsWithChildren<  Omit<LinkProps, "href"> &    Omit<ComponentProps<"a">, "href"> & { href: string; locale?: Locale }>;export default function LocalizedLink({  href,  locale,  children,  ...props}: LocalizedLinkProps) {  const params = useParams();  const fallback = (params?.locale as Locale | undefined) ?? defaultLocale;  const normalizedLocale = (locales as readonly string[]).includes(fallback)    ? ((locale ?? fallback) as Locale)    : defaultLocale;  const normalizedPath = href.startsWith("/") ? href : `/${href}`;  const localizedHref = isExternal(href)    ? href    : localizedPath(normalizedLocale, normalizedPath);  return (    <NextLink href={localizedHref} {...props}>      {children}    </NextLink>  );}
    Совет: Поскольку LocalizedLink является заменой "drop-in", мигрируйте постепенно, меняя импорты и позволяя компоненту обрабатывать URL-адреса, специфичные для локали.

    (Опционально) Шаг 14: Доступ к активной локали внутри Server Actions

    Server Actions часто нуждаются в текущей локали для писем, логирования или интеграций с третьими сторонами. Объедините cookie с локалью, установленной вашим прокси, с заголовком Accept-Language в качестве резервного варианта.

    src/app/actions/get-current-locale.ts
    "use server";import { cookies, headers } from "next/headers";import { defaultLocale, locales, type Locale } from "@/i18n.config";const KNOWN_LOCALES = new Set(locales as readonly string[]);const normalize = (value: string | undefined): Locale | undefined => {  if (!value) return undefined;  const base = value.toLowerCase().split("-")[0];  return KNOWN_LOCALES.has(base) ? (base as Locale) : undefined;};export async function getCurrentLocale(): Promise<Locale> {  const cookieLocale = normalize(cookies().get("NEXT_LOCALE")?.value);  if (cookieLocale) return cookieLocale;  const headerLocale = normalize(headers().get("accept-language"));  return headerLocale ?? defaultLocale;}// Пример server action, который использует текущую локальexport async function stuffFromServer(formData: FormData) {  const locale = await getCurrentLocale();  // Используйте локаль для локализованных побочных эффектов (электронные письма, CRM и т.д.)  console.log(`Stuff from server with locale ${locale}`);}
    Поскольку помощник использует cookies и заголовки Next.js, он работает в Route Handlers, Server Actions и других контекстах, доступных только на сервере.

    (Необязательно) Шаг 15: Интернационализация ваших метаданных

    Перевод контента важен, но основная цель интернационализации — сделать ваш сайт более заметным для всего мира. I18n — это мощный инструмент для улучшения видимости вашего сайта через правильное SEO.

    Правильно интернационализированные метаданные помогают поисковым системам понять, какие языки доступны на ваших страницах. Это включает установку метатегов hreflang, перевод заголовков и описаний, а также обеспечение корректной установки канонических URL для каждой локали.

    Вот список лучших практик по многоязычному SEO:

    • Устанавливайте метатеги hreflang в теге <head>, чтобы помочь поисковым системам понять, какие языки доступны на странице
    • Перечисляйте все переводы страниц в sitemap.xml, используя XML-схему http://www.w3.org/1999/xhtml
    • Не забывайте исключать страницы с префиксами из robots.txt (например, /dashboard, /fr/dashboard, /es/dashboard)
    • Используйте кастомный компонент Link для перенаправления на наиболее локализованную страницу (например, на французском <a href="/fr/about">À propos</a>)

    Разработчики часто забывают правильно ссылаться на свои страницы в разных локалях. Исправим это:

    src/app/[locale]/about/layout.tsx
    import type { Metadata } from "next";import {  locales,  defaultLocale,  localizedPath,  absoluteUrl,} from "@/i18n.config";/** * Генерирует SEO метаданные для каждой локальной версии страницы * Эта функция выполняется для каждого локали во время сборки */export async function generateMetadata({  params,}: {  params: { locale: string };}): Promise<Metadata> {  const { locale } = params;  // Динамически импортировать файл перевода для этой локали  // Используется для получения переведённого заголовка и описания для метаданных  const messages = (await import(`@/locales/${locale}/about.json`)).default;  // Создать отображение hreflang для всех локалей  // Помогает поисковым системам понять языковые альтернативы  // Формат: { "en": "/about", "fr": "/fr/about" }  const languages = Object.fromEntries(    locales.map((locale) => [locale, localizedPath(locale, "/about")])  );  return {    title: messages.title,    description: messages.description,    alternates: {      // Канонический URL для этой локальной версии      canonical: absoluteUrl(locale, "/about"),      // Языковые альтернативы для SEO (теги hreflang)      // "x-default" указывает версию локали по умолчанию      languages: {        ...languages,        "x-default": absoluteUrl(defaultLocale, "/about"),      },    },  };}export default async function AboutPage() {  return <h1>О нас</h1>;}

    (Необязательно) Шаг 16: Интернационализация вашей карты сайта

    Создайте карту сайта, которая включает все версии страниц для каждой локали. Это помогает поисковым системам обнаруживать и индексировать все языковые версии вашего контента.

    Правильно интернационализированная карта сайта гарантирует, что поисковые системы смогут найти и индексировать все языковые версии ваших страниц. Это улучшает видимость в международных результатах поиска.

    src/app/sitemap.ts
    import type { MetadataRoute } from "next";import { defaultLocale, locales } from "@/i18n";const origin = "https://example.com";// Форматирование локализованного пути в зависимости от локалиconst formatterLocalizedPath = (locale: string, path: string) =>  locale === defaultLocale ? `${origin}${path}` : `${origin}/${locale}${path}`;/** * Получить карту всех локалей и их локализованных путей * * Пример вывода: * { *   "en": "https://example.com", *   "fr": "https://example.com/fr", *   "es": "https://example.com/es", *   "x-default": "https://example.com" * } */const getLocalizedMap = (path: string) =>  Object.fromEntries([    ...locales.map((locale) => [locale, formatterLocalizedPath(locale, path)]),    ["x-default", formatterLocalizedPath(defaultLocale, path)],  ]);// Генерация карты сайта со всеми вариантами локалей для улучшения SEO// Поле alternates сообщает поисковым системам о языковых версияхexport default function sitemap(): MetadataRoute.Sitemap {  return [    {      url: formatterLocalizedPath(defaultLocale, "/"),      lastModified: new Date(),      changeFrequency: "monthly",      priority: 1.0,      alternates: { languages: getLocalizedMap("/") },    },    {      url: formatterLocalizedPath(defaultLocale, "/about"),      lastModified: new Date(),      changeFrequency: "monthly",      priority: 0.7,      alternates: { languages: getLocalizedMap("/about") },    },  ];}

    (Необязательно) Шаг 17: Интернационализация вашего файла robots.txt

    Создайте файл robots.txt, который корректно обрабатывает все языковые версии ваших защищённых маршрутов. Это гарантирует, что поисковые системы не будут индексировать страницы админки или панели управления на любом языке.

    Правильная настройка robots.txt для всех локалей предотвращает индексацию поисковыми системами конфиденциальных страниц на любом языке. Это важно для безопасности и конфиденциальности.

    src/app/robots.ts
    import type { MetadataRoute } from "next";import { defaultLocale, locales } from "@/i18n";const origin = "https://example.com";// Генерируем пути для всех локалей (например, /admin, /fr/admin, /es/admin)const withAllLocales = (path: string) => [  path,  ...locales    .filter((locale) => locale !== defaultLocale)    .map((locale) => `/${locale}${path}`),];const disallow = [...withAllLocales("/dashboard"), ...withAllLocales("/admin")];export default function robots(): MetadataRoute.Robots {  return {    rules: { userAgent: "*", allow: ["/"], disallow },    host: origin,    sitemap: `${origin}/sitemap.xml`,  };}

    (Необязательно) Шаг 18: Настройка Middleware для маршрутизации по локали

    Создайте прокси, который автоматически определяет предпочитаемую пользователем локаль и перенаправляет его на соответствующий URL с префиксом локали. Это улучшает пользовательский опыт, показывая контент на предпочитаемом языке.

    Middleware гарантирует, что пользователи автоматически перенаправляются на предпочитаемый язык при посещении вашего сайта. Также сохраняет предпочтение пользователя в cookie для будущих посещений.

    src/proxy.ts
    import { NextResponse, type NextRequest } from "next/server";import { defaultLocale, locales } from "@/i18n.config";// Регулярное выражение для сопоставления файлов с расширениями (например, .js, .css, .png)// Используется для исключения статических ресурсов из маршрутизации по локалиconst PUBLIC_FILE = /\.[^/]+$/;/** * Извлекает локаль из заголовка Accept-Language * Обрабатывает форматы, такие как "fr-CA", "en-US" и т.д. * Возвращает локаль по умолчанию, если язык браузера не поддерживается */const pickLocale = (accept: string | null) => {  // Получить предпочтительный язык пользователя (например, "fr-CA" из "fr-CA,en-US;q=0.9")  const raw = accept?.split(",")[0] ?? defaultLocale;  // Извлечь основной код языка (например, "fr" из "fr-CA")  const base = raw.toLowerCase().split("-")[0];  // Проверить, поддерживаем ли мы эту локаль, иначе использовать локаль по умолчанию  return (locales as readonly string[]).includes(base) ? base : defaultLocale;};/** * Прокси Next.js для определения локали и маршрутизации * Выполняется при каждом запросе перед рендерингом страницы * Автоматически перенаправляет на URL с префиксом локали при необходимости */export function proxy(request: NextRequest) {  const { pathname } = request.nextUrl;  // Пропустить прокси для внутренних маршрутов Next.js, API маршрутов и статических файлов  // Для них не нужен префикс локали  if (    pathname.startsWith("/_next") ||    pathname.startsWith("/api") ||    pathname.startsWith("/static") ||    PUBLIC_FILE.test(pathname)  ) {    return;  }  // Проверить, есть ли в URL уже префикс локали  // Пример: "/fr/about" или "/en" вернут true  const hasLocale = (locales as readonly string[]).some(    (locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)  );  // Если префикс локали отсутствует, определить локаль и выполнить перенаправление  if (!hasLocale) {    // Сначала попытаться получить локаль из cookie (предпочтение пользователя)    const cookieLocale = request.cookies.get("NEXT_LOCALE")?.value;    // Использовать локаль из cookie, если она валидна, иначе определить из заголовков браузера    const locale =      cookieLocale && (locales as readonly string[]).includes(cookieLocale)        ? cookieLocale        : pickLocale(request.headers.get("accept-language"));    // Клонировать URL для изменения pathname    const url = request.nextUrl.clone();    // Добавить префикс локали к pathname    // Особая обработка корневого пути, чтобы избежать двойного слеша    url.pathname = `/${locale}${pathname === "/" ? "" : pathname}`;    // Создать ответ с перенаправлением и установить cookie локали    const res = NextResponse.redirect(url);    res.cookies.set("NEXT_LOCALE", locale, { path: "/" });    return res;  }}export const config = {  matcher: [    // Совпадение со всеми путями, кроме:    // - API маршрутов (/api/*)    // - Внутренних путей Next.js (/_next/*)    // - Статических файлов (/static/*)    // - Файлов с расширениями (.*\\..*)    "/((?!api|_next|static|.*\\..*).*)",  ],};

    (Опционально) Шаг 19: Автоматизируйте ваши переводы с помощью Intlayer

    Intlayer — это бесплатная и открытая библиотека, созданная для помощи в процессе локализации вашего приложения. В то время как i18next отвечает за загрузку и управление переводами, Intlayer помогает автоматизировать рабочий процесс перевода.

    Управление переводами вручную может занимать много времени и быть подвержено ошибкам. Intlayer автоматизирует тестирование, генерацию и управление переводами, экономя ваше время и обеспечивая согласованность по всему приложению.

    Intlayer позволяет вам:

    • Объявлять ваш контент там, где вы хотите, в вашем коде Intlayer позволяет объявлять ваш контент там, где вы хотите, в вашем коде, используя файлы .content.{ts|js|json}. Это обеспечит лучшую организацию вашего контента, улучшая читаемость и поддерживаемость вашей codebase.

    • Тестировать отсутствующие переводы Intlayer предоставляет функции тестирования, которые можно интегрировать в ваш CI/CD pipeline или в ваши модульные тесты. Узнайте больше о тестировании ваших переводов.

    • Автоматизация ваших переводов, Intlayer предоставляет CLI и расширение для VSCode для автоматизации ваших переводов. Это можно интегрировать в ваш CI/CD pipeline. Узнайте больше о автоматизации ваших переводов. Вы можете использовать свой собственный API-ключ и выбранного вами AI-провайдера. Также поддерживаются контекстно-зависимые переводы, см. заполнение контента.

    • Подключение внешнего контента
    • Автоматизация переводов
      Intlayer предоставляет CLI и расширение для VSCode для автоматизации ваших переводов. Это можно интегрировать в ваш CI/CD pipeline. Узнайте больше о автоматизации переводов.
      Вы можете использовать собственный API ключ и выбранного вами AI провайдера. Также поддерживаются контекстно-зависимые переводы, смотрите заполнение контента.

    • Подключение внешнего контента
      Intlayer позволяет подключать ваш контент к внешней системе управления контентом (CMS). Для оптимального получения данных и вставки их в ваши JSON ресурсы. Узнайте больше о получении внешнего контента.

    • Визуальный редактор
      Intlayer предлагает бесплатный визуальный редактор для редактирования вашего контента. Узнайте больше о визуальном редактировании переводов.

    И многое другое. Чтобы узнать обо всех возможностях Intlayer, пожалуйста, обратитесь к документации по интересу Intlayer.