Автор:
    Создание:2025-08-23Последнее обновление:2026-05-31

    next-i18next ПРОТИВ next-intl ПРОТИВ intlayer | Интернационализация (i18n) Next.js

    Давайте рассмотрим сходства и различия между тремя вариантами i18n для Next.js: next-i18next, next-intl и Intlayer.

    Это не полный учебник. Это сравнение, чтобы помочь вам сделать выбор.

    Мы сосредотачиваемся на Next.js 13+ App RouterReact Server Components) и оцениваем:

    1. Архитектура и организация контента
    2. TypeScript и безопасность
    3. Обработка отсутствующих переводов
    4. Маршрутизация и промежуточное ПО
    5. Производительность и поведение при загрузке
    6. Опыт разработчика (DX), инструменты и сопровождение
    7. SEO и масштабируемость для крупных проектов
    Кратко: Все три решения могут локализовать приложение Next.js. Если вам нужен контент, ограниченный компонентом, строгие типы TypeScript, проверки отсутствующих ключей во время сборки, дерево-шейкнутые словари и первоклассная поддержка App Router + SEO помощников, то Intlayer, самый полный и современный выбор.
    Одна из распространённых ошибок разработчиков, считать, что next-intl, это версия react-intl для Next.js. Это не так, next-intl поддерживается Amann, а react-intl поддерживается FormatJS.

    Кратко

    • next-intl, лёгкое и простое форматирование сообщений с надёжной поддержкой Next.js. Обычно используются централизованные каталоги; опыт разработчика (DX) простой, но безопасность и масштабное сопровождение в основном остаются вашей ответственностью.
    • next-i18next, i18next в обёртке Next.js. Зрелая экосистема и функции через плагины (например, ICU), но конфигурация может быть многословной, а каталоги имеют тенденцию к централизации по мере роста проектов.
    • Intlayer, компонентно-ориентированная модель контента для Next.js, строгая типизация TS, проверки во время сборки, tree-shaking, встроенные middleware и SEO-помощники, опциональный Визуальный редактор/CMS и переводы с помощью ИИ.

    Library GitHub Stars Total Commits Last Commit First Version NPM Version NPM Downloads
    aymericzip/intlayer GitHub Repo stars GitHub commit activity Last Commit April 2024 npm npm downloads
    amannn/next-intl GitHub Repo stars GitHub commit activity Last Commit Nov 2020 npm npm downloads
    i18next/i18next GitHub Repo stars GitHub commit activity Last Commit Jan 2012 npm npm downloads
    i18next/next-i18next GitHub Repo stars GitHub commit activity Last Commit Nov 2018 npm npm downloads
    Значки обновляются автоматически. Снимки будут меняться со временем.

    Сравнение функций бок о бок (с акцентом на Next.js)

    Функция next-intlayer (Intlayer) next-intl next-i18next
    Переводы рядом с компонентами ✅ Да, контент расположен вместе с каждым компонентом ❌ Нет ❌ Нет
    Интеграция с TypeScript ✅ Продвинутая, автоматически сгенерированные строгие типы ✅ Хорошая ⚠️ Базовая
    Обнаружение отсутствующих переводов ✅ Подсветка ошибок TypeScript и ошибки/предупреждения во время сборки ⚠️ Запасной вариант во время выполнения ⚠️ Запасной вариант во время выполнения
    Богатый контент (JSX/Markdown/компоненты) ✅ Прямая поддержка ❌ Не предназначено для сложных узлов ⚠️ Ограниченно
    Перевод с использованием ИИ ✅ Да, поддерживает нескольких провайдеров ИИ. Можно использовать с собственными API-ключами. Учитывает контекст вашего приложения и объем контента ❌ Нет ❌ Нет
    Визуальный редактор ✅ Да, локальный визуальный редактор + опциональная CMS; может вынести содержимое кодовой базы; встраиваемый ❌ Нет / доступно через внешние платформы локализации ❌ Нет / доступно через внешние платформы локализации
    Локализованный роутинг ✅ Да, поддерживает локализованные пути из коробки (работает с Next.js и Vite) ✅ Встроенный, App Router поддерживает сегмент [locale] ✅ Встроенный
    Генерация динамических маршрутов ✅ Да ✅ Да ✅ Да
    Плюрализация ✅ Шаблоны на основе перечислений ✅ Хорошо ✅ Хорошо
    Форматирование (даты, числа, валюты) ✅ Оптимизированные форматтеры (Intl под капотом) ✅ Хорошо (помощники Intl) ✅ Хорошо (помощники Intl)
    Формат контента ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml в разработке) ✅ .json, .js, .ts ⚠️ .json
    Поддержка ICU ⚠️ В разработке ✅ Да ⚠️ Через плагин (i18next-icu)
    SEO помощники (hreflang, sitemap) ✅ Встроенные инструменты: помощники для sitemap, robots.txt, метаданных ✅ Хорошо ✅ Хорошо
    Экосистема / Сообщество ⚠️ Меньше, но быстро растет и активно развивается ✅ Хорошо ✅ Хорошо
    Серверный рендеринг и серверные компоненты ✅ Да, оптимизировано для SSR / React Server Components ⚠️ Поддерживается на уровне страниц, но необходимо передавать t-функции в дерево компонентов для дочерних серверных компонентов ⚠️ Поддерживается на уровне страниц, но необходимо передавать t-функции в дерево компонентов для дочерних серверных компонентов
    Tree-shaking (загрузка только используемого контента) ✅ Да, по компонентам во время сборки через плагины Babel/SWC ⚠️ Частично ⚠️ Частично
    Ленивая загрузка ✅ Да, по локалям / по словарям ✅ Да (по маршруту/по локали), требуется управление пространствами имён ✅ Да (по маршруту/по локали), требуется управление пространствами имён
    Очистка неиспользуемого контента ✅ Да, по словарю во время сборки ❌ Нет, можно управлять вручную с помощью управления пространствами имён ❌ Нет, можно управлять вручную с помощью управления пространствами имён
    Управление крупными проектами ✅ Поощряет модульность, подходит для дизайн-систем ✅ Модульное с настройкой ✅ Модульное с настройкой
    Тестирование отсутствующих переводов (CLI/CI) ✅ CLI: npx intlayer content test (аудит, удобный для CI) ⚠️ Не встроено; в документации предлагается npx @lingual/i18n-check ⚠️ Не встроено; полагаться на инструменты i18next / runtime saveMissing

    Введение

    Next.js предоставляет встроенную поддержку интернационализированной маршрутизации (например, сегменты локали). Но эта функция сама по себе не выполняет переводы. Вам все равно нужна библиотека для отображения локализованного контента вашим пользователям.

    Существует множество библиотек для i18n, но в мире Next.js сегодня три из них набирают популярность: next-i18next, next-intl и Intlayer.


    Архитектура и масштабируемость

    • next-intl / next-i18next: По умолчанию используют централизованные каталоги для каждой локали (плюс пространства имён в i18next). Хорошо работает на начальных этапах, но часто становится большой общей областью с растущей связанностью и частой сменой ключей.
    • Intlayer: Поощряет использование словарей на уровне компонентов (или функций), расположенных рядом с кодом, который они обслуживают. Это снижает когнитивную нагрузку, облегчает дублирование/миграцию частей интерфейса и уменьшает конфликты между командами. Неиспользуемый контент естественным образом легче обнаружить и удалить.

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


    Размеры бандлов и зависимости

    После сборки приложения, бандл, это JavaScript, который браузер загрузит для отображения страницы. Размер бандла, следовательно, важен для производительности приложения.

    В контексте бандла многоязычного приложения важны два компонента:

    • Код приложения
    • Контент, загружаемый браузером

    Код приложения

    Важность кода приложения в данном случае минимальна. Все три решения поддерживают tree-shaking, что означает, что неиспользуемые части кода не включаются в бандл.

    Вот сравнение размера JavaScript-бандла, загружаемого браузером для многоязычного приложения с использованием трех решений.

    Если в приложении не нужен никакой форматтер, список экспортируемых функций после tree-shaking будет следующим:

    • next-intlayer: useIntlayer, useLocale, NextIntlClientProvider, (Размер бандла 180.6 кБ -> 78.6 кБ (gzip))
    • next-intl: useTranslations, useLocale, NextIntlClientProvider, (Размер бандла 101.3 кБ -> 31.4 кБ (gzip))
    • next-i18next: useTranslation, useI18n, I18nextProvider, (Размер бандла 80.7 кБ -> 25.5 кБ (gzip))

    Эти функции являются лишь обёртками вокруг контекста/состояния React, поэтому общий вклад библиотеки i18n в размер бандла минимален.

    Intlayer немного больше, чем next-intl и next-i18next, потому что включает больше логики в функции useIntlayer. Это связано с интеграцией markdown и intlayer-editor.

    Контент и переводы

    Эта часть часто игнорируется разработчиками, но давайте рассмотрим случай приложения, состоящего из 10 страниц на 10 языках. Предположим, что каждая страница содержит 100% уникального контента для упрощения расчёта (на самом деле, много контента повторяется между страницами, например, заголовок страницы, шапка, подвал и т.д.).

    Пользователь, желающий посетить страницу /fr/about, загрузит контент одной страницы на данном языке. Игнорирование оптимизации контента означало бы ненужную загрузку 8200% ((1 + (((10 страниц - 1) × (10 языков - 1)))) × 100) контента приложения. Видите проблему? Даже если этот контент остаётся текстом, и хотя вы, вероятно, предпочитаете думать об оптимизации изображений на вашем сайте, вы отправляете бесполезный контент по всему миру и заставляете компьютеры пользователей обрабатывать его зря.

    Две важные задачи:

    • Разделение по маршруту:

      Если я нахожусь на странице /about, я не хочу загружать содержимое страницы /home
    • Разделение по локали:

      Если я нахожусь на странице /fr/about, я не хочу загружать содержимое страницы /en/about

    Опять же, все три решения осведомлены об этих проблемах и позволяют управлять этими оптимизациями. Разница между тремя решениями заключается в опыте разработчика (DX).

    next-intl и next-i18next используют централизованный подход к управлению переводами, позволяющий разделять JSON по локалям и по подфайлам. В next-i18next мы называем JSON-файлы «пространствами имён» (namespaces); next-intl позволяет объявлять сообщения. В intlayer мы называем JSON-файлы «словарями» (dictionaries).

    • В случае с next-intl, как и с next-i18next, контент загружается на уровне страницы/макета, затем этот контент загружается в провайдер контекста. Это означает, что разработчик должен вручную управлять JSON-файлами, которые будут загружены для каждой страницы.
    На практике это означает, что разработчики часто пропускают эту оптимизацию, предпочитая для простоты загружать весь контент в провайдер контекста страницы.
    • В случае с intlayer весь контент загружается в приложении. Затем плагин (@intlayer/babel / @intlayer/swc) заботится об оптимизации бандла, загружая только тот контент, который используется на странице. Следовательно, разработчику не нужно вручную управлять словарями, которые будут загружены. Это позволяет добиться лучшей оптимизации, лучшей поддерживаемости и сокращает время разработки.

    По мере роста приложения (особенно когда над приложением работают несколько разработчиков) часто забывают удалять из JSON-файлов контент, который больше не используется.

    Обратите внимание, что во всех случаях загружается весь JSON (next-intl, next-i18next, intlayer).

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

    Также важно, как библиотека обрабатывает fallback. Рассмотрим, что приложение по умолчанию на английском языке, и пользователь заходит на страницу /fr/about. Если переводы на французском отсутствуют, будет использоваться английский fallback.

    В случае с next-intl и next-i18next библиотека требует загрузки JSON, связанного с текущей локалью, а также с локалью-резервом. Таким образом, учитывая, что весь контент переведен, каждая страница загрузит 100% ненужного контента. В сравнении с этим, intlayer обрабатывает резервный вариант на этапе сборки словаря. Таким образом, каждая страница загрузит только используемый контент.

    Вот пример влияния оптимизации размера бандла с использованием intlayer в приложении на vite + react:

    Оптимизированный бандл Неоптимизированный бандл
    оптимизированный бандл неоптимизированный бандл

    TypeScript и безопасность

    next-intl

    • Надёжная поддержка TypeScript, но ключи по умолчанию не строго типизированы; вам придётся вручную поддерживать паттерны безопасности.

    next-i18next

    • Базовые типы для хуков; строгая типизация ключей требует дополнительного инструментария/конфигурации.

    intlayer

    • Генерирует строгие типы на основе вашего контента. Автодополнение в IDE и ошибки на этапе компиляции ловят опечатки и отсутствующие ключи до деплоя.

    Почему это важно: Строгая типизация сдвигает ошибки влево (CI/сборка), а не вправо (во время выполнения).


    Обработка отсутствующих переводов

    next-intl

    • Опирается на запасные варианты во время выполнения (например, показывает ключ или локаль по умолчанию). Сборка не прерывается.

    next-i18next

    • Опирается на запасные варианты во время выполнения (например, показывает ключ или локаль по умолчанию). Сборка не прерывается.

    intlayer

    • Обнаружение на этапе сборки с предупреждениями/ошибками для отсутствующих локалей или ключей.

    Почему это важно: Обнаружение пробелов во время сборки предотвращает появление «непонятных строк» в продакшене и соответствует строгим требованиям к выпуску.


    Маршрутизация, middleware и стратегия URL

    next-intl

    • Работает с локализованной маршрутизацией Next.js на App Router.

    next-i18next

    • Работает с локализованной маршрутизацией Next.js на App Router.

    intlayer

    • Всё вышеперечисленное, плюс i18n middleware (определение локали через заголовки/куки) и хелперы для генерации локализованных URL и тегов <link rel="alternate" hreflang="…">.

    Почему это важно: Меньше пользовательских прослоек; последовательный UX и чистое SEO для всех локалей.


    Совместимость с серверными компонентами (RSC)

    next-intl

    • Поддерживает Next.js 13+. Часто требует передачи t-функций/форматтеров через дерево компонентов в гибридных настройках.

    next-i18next

    • Поддерживает Next.js 13+. Аналогичные ограничения при передаче утилит перевода через границы.

    intlayer

    • Поддерживает Next.js 13+ и сглаживает границу сервер/клиент с помощью единого API и провайдеров, ориентированных на RSC, избегая передачи форматтеров или функций t.

    Почему это важно: Более чистая ментальная модель и меньше крайних случаев в гибридных деревьях.


    Интеграция с платформами локализации (TMS)

    Крупные организации часто используют Системы Управления Переводами (TMS), такие как Crowdin, Phrase, Lokalise, Localizely или Localazy.

    • Почему это важно для компаний

      • Сотрудничество и роли: В процесс вовлечены разные участники: разработчики, менеджеры продуктов, переводчики, рецензенты, маркетинговые команды.
      • Масштаб и эффективность: непрерывная локализация, проверка в контексте.
    • next-intl / next-i18next

      • Обычно используют централизованные JSON-каталоги, поэтому экспорт/импорт с TMS является простым.
      • Зрелые экосистемы и примеры/интеграции для перечисленных платформ.
    • Intlayer

      • Поощряет использование децентрализованных словарей по компонентам и поддерживает контент в форматах TypeScript/TSX/JS/JSON/MD.
      • Это улучшает модульность кода, но может усложнить интеграцию с TMS, когда инструмент ожидает централизованные, плоские JSON-файлы.
      • Intlayer предлагает альтернативы: переводы с помощью ИИ (с использованием ваших собственных ключей провайдера), Визуальный редактор/CMS и рабочие процессы CLI/CI для обнаружения и предварительного заполнения пробелов.
    Примечание: next-intl и i18next также поддерживают каталоги на TypeScript. Если ваша команда хранит сообщения в файлах .ts или децентрализует их по функционалу, вы можете столкнуться с аналогичными трудностями при интеграции с TMS. Однако многие настройки next-intl остаются централизованными в папке locales/, что немного облегчает рефакторинг в JSON для TMS.

    Опыт разработчика

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

    Структура приложения

    Структура приложения важна для обеспечения хорошей поддерживаемости вашего кода.

    bash
    .
    ├── i18n.config.ts
    └── src
        ├── locales
       ├── en
      ├── common.json
      └── about.json
       └── fr
          ├── common.json
          └── about.json
        ├── app
       ├── i18n
       └── server.ts
       └── [locale]
           ├── layout.tsx
           └── about.tsx
        └── components
            ├── I18nProvider.tsx
            ├── ClientComponent.tsx
            └── ServerComponent.tsx
    
    bash
    .
    ├── i18n.ts
    ├── locales
       ├── en
      ├── home.json
      └── navbar.json
       ├── fr
      ├── home.json
      └── navbar.json
       └── es
          ├── home.json
          └── navbar.json
    └── src
        ├── middleware.ts
        ├── app
       ├── i18n
       └── server.ts
       └── [locale]
           └── home.tsx
        └── components
            └── Navbar
                └── index.tsx
    
    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── middleware.ts
        ├── app
       └── [locale]
           ├── layout.tsx
           └── home
               ├── index.tsx
               └── index.content.ts
        └── components
            └── Navbar
                ├── index.tsx
                └── index.content.ts
    

    Сравнение

    • next-intl / next-i18next: Централизованные каталоги (JSON; пространства имён/сообщения). Чёткая структура, хорошо интегрируется с платформами перевода, но может привести к большему количеству изменений в разных файлах по мере роста приложений.
    • Intlayer: Словари .content.{ts|js|json} для каждого компонента, расположенные рядом с компонентами. Облегчает повторное использование компонентов и локальное понимание; добавляет файлы и опирается на инструменты сборки.

    Настройка и загрузка контента

    Как упоминалось ранее, вы должны оптимизировать способ импорта каждого JSON-файла в ваш код. Важен способ, которым библиотека обрабатывает загрузку контента.

    next-i18next.config.js
    module.exports = {
      i18n: {
        locales: ["en", "fr", "es"],
        defaultLocale: "en",
      },
    };
    
    src/app/_app.tsx
    import { appWithTranslation } from "next-i18next";
    
    const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
    
    export default appWithTranslation(MyApp);
    
    src/app/[locale]/about/page.tsx
    import type { GetStaticProps } from "next";
    import { serverSideTranslations } from "next-i18next/serverSideTranslations";
    import { useTranslation } from "next-i18next";
    import { I18nextProvider, initReactI18next } from "react-i18next";
    import { createInstance } from "i18next";
    import { ClientComponent, ServerComponent } from "@components";
    
    export default function HomePage({ locale }: { locale: string }) {
      // Явно укажите пространство имён, используемое этим компонентом
      const resources = await loadMessagesFor(locale); // ваш загрузчик (JSON и т.д.)
    
      const i18n = createInstance();
      i18n.use(initReactI18next).init({
        lng: locale,
        fallbackLng: "en",
        resources,
        ns: ["common", "about"],
        defaultNS: "common",
        interpolation: { escapeValue: false },
      });
    
      const { t } = useTranslation("about");
    
      return (
        <I18nextProvider i18n={i18n}>
          <main>
            <h1>{t("title")}</h1>
            <ClientComponent />
            <ServerComponent />
          </main>
        </I18nextProvider>
      );
    }
    
    export const getStaticProps: GetStaticProps = async ({ locale }) => {
      // Предзагружайте только необходимые пространства имён для ЭТОЙ страницы
      return {
        props: {
          ...(await serverSideTranslations(locale ?? "en", ["common", "about"])),
        },
      };
    };
    
    i18n.ts
    import { getRequestConfig } from "next-intl/server";
    import { notFound } from "next/navigation";
    
    // Может быть импортировано из общей конфигурации
    const locales = ["en", "fr", "es"];
    
    export default getRequestConfig(async ({ locale }) => {
      // Проверяем, что входящий параметр `locale` является допустимым
      if (!locales.includes(locale)) notFound();
    
      return {
        messages: (await import(`../messages/${locale}.json`)).default,
      };
    });
    
    src/app/[locale]/about/layout.tsx
    import { NextIntlClientProvider } from "next-intl";
    import { getMessages, unstable_setRequestLocale } from "next-intl/server";
    import pick from "lodash/pick";
    
    export default async function LocaleLayout({
      children,
      params,
    }: {
      children: React.ReactNode;
      params: { locale: string };
    }) {
      const { locale } = params;
    
      // Установить активную локаль запроса для этого серверного рендера (RSC)
      unstable_setRequestLocale(locale);
    
      // Сообщения загружаются на стороне сервера через src/i18n/request.ts
      // (см. документацию next-intl). Здесь мы передаем на клиент
      // только подмножество, необходимое для клиентских компонентов (оптимизация нагрузки).
      const messages = await getMessages();
      const clientMessages = pick(messages, ["common", "about"]);
    
      const rtlLocales = ["ar", "he", "fa", "ur"];
    
      return (
        <html lang={locale} dir={rtlLocales.includes(locale) ? "rtl" : "ltr"}>
          <body>
            <NextIntlClientProvider locale={locale} messages={clientMessages}>
              {children}
            </NextIntlClientProvider>
          </body>
        </html>
      );
    }
    
    src/app/[locale]/about/page.tsx
    import { getTranslations } from "next-intl/server";
    import { ClientComponent, ServerComponent } from "@components";
    
    export default async function LandingPage({
      params,
    }: {
      params: { locale: string };
    }) {
      // Загрузка строго на стороне сервера (без гидратации на клиенте)
      const t = await getTranslations("about");
    
      return (
        <main>
          <h1>{t("title")}</h1>
          <ClientComponent />
          <ServerComponent />
        </main>
      );
    }
    
    intlayer.config.ts
    export default {
      internationalization: {
        locales: ["en", "fr", "es"],
        defaultLocale: "en",
      },
    };
    
    src/app/[locale]/layout.tsx
    import { getHTMLTextDir } from "intlayer";
    import {
      IntlayerClientProvider,
      generateStaticParams,
      type NextLayoutIntlayer,
    } from "next-intlayer";
    
    export const dynamic = "force-static";
    
    const LandingLayout: NextLayoutIntlayer = async ({ children, params }) => {
      const { locale } = await params;
    
      return (
        <html lang={locale} dir={getHTMLTextDir(locale)}>
          <IntlayerClientProvider locale={locale}>
            {children}
          </IntlayerClientProvider>
        </html>
      );
    };
    
    export default LandingLayout;
    
    src/app/[locale]/about/page.tsx
    import { PageContent } from "@components/PageContent";
    import type { NextPageIntlayer } from "next-intlayer";
    import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";
    import { ClientComponent, ServerComponent } from "@components";
    
    const LandingPage: NextPageIntlayer = async ({ params }) => {
      const { locale } = await params;
      const { title } = useIntlayer("about", locale);
    
      return (
        <IntlayerServerProvider locale={locale}>
          <main>
            <h1>{title}</h1>
            <ClientComponent />
            <ServerComponent />
          </main>
        </IntlayerServerProvider>
      );
    };
    
    export default LandingPage;
    

    Сравнение

    Все три решения поддерживают загрузку контента и провайдеры для каждой локали.

    • С next-intl/next-i18next вы обычно загружаете выбранные сообщения/пространства имён для каждого маршрута и размещаете провайдеры там, где это необходимо.

    • С Intlayer добавляется анализ во время сборки для определения использования, что может сократить ручное подключение и позволить использовать один корневой провайдер.

    Выбирайте между явным контролем и автоматизацией в зависимости от предпочтений вашей команды.

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

    Рассмотрим пример клиентского компонента, который отображает счётчик.

    Переводы (должны быть настоящим JSON в public/locales/...)

    public/locales/en/about.json
    {
      "counter": {
        "label": "Счётчик",
        "increment": "Увеличить"
      }
    }
    
    public/locales/fr/about.json
    {
      "counter": {
        "label": "Счётчик",
        "increment": "Увеличить"
      }
    }
    

    Клиентский компонент

    src/components/ClientComponentExample.tsx
    "use client";
    
    import React, { useMemo, useState } from "react";
    import { useTranslation } from "next-i18next";
    
    const ClientComponentExample = () => {
      const { t, i18n } = useTranslation("about");
      const [count, setCount] = useState(0);
    
      // next-i18next не предоставляет useNumber; используйте Intl.NumberFormat
      const numberFormat = new Intl.NumberFormat(i18n.language);
    
      return (
        <div>
          <p>{numberFormat.format(count)}</p>
          <button
            aria-label={t("counter.label")}
            onClick={() => setCount((count) => count + 1)}
          >
            {t("counter.increment")}
          </button>
        </div>
      );
    };
    

    Не забудьте добавить пространство имён "about" на странице serverSideTranslations
    Здесь используется версия React 19.x.x, но для более низких версий вам потребуется использовать useMemo для хранения экземпляра форматтера, так как это тяжёлая функция

    Переводы (используется та же структура; загружайте их в сообщения next-intl по своему усмотрению)

    locales/en/about.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    locales/fr/about.json
    {
      "counter": {
        "label": "Compteur",
        "increment": "Incrémenter"
      }
    }
    

    Клиентский компонент

    src/components/ClientComponentExample.tsx
    "use client";
    
    import React, { useState } from "react";
    import { useTranslations, useFormatter } from "next-intl";
    
    const ClientComponentExample = () => {
      // Область видимости непосредственно для вложенного объекта
      const t = useTranslations("about.counter");
      const format = useFormatter();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{format.number(count)}</p>
          <button
            aria-label={t("label")}
            onClick={() => setCount((count) => count + 1)}
          >
            {t("increment")}
          </button>
        </div>
      );
    };
    
    Не забудьте добавить сообщение "about" в клиентские сообщения страницы

    Содержание

    src/components/ClientComponentExample/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ ru: "Счётчик", en: "Counter", fr: "Compteur" }),
        increment: t({ ru: "Увеличить", en: "Increment", fr: "Incrémenter" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    

    Клиентский компонент

    src/components/ClientComponentExample/index.tsx
    "use client";
    
    import React, { useState } from "react";
    import { useNumber, useIntlayer } from "next-intlayer";
    
    const ClientComponentExample = () => {
      const [count, setCount] = useState(0);
      const { label, increment } = useIntlayer("counter"); // возвращает строки
      const { number } = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label} onClick={() => setCount((count) => count + 1)}>
            {increment}
          </button>
        </div>
      );
    };
    

    Сравнение

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

      • next-i18next: нет useNumber; используется Intl.NumberFormat (или i18next-icu).
      • next-intl: useFormatter().number(value).
      • Intlayer: встроенный useNumber().
    • Ключи

      • Сохраняйте вложенную структуру (about.counter.label) и используйте хук с соответствующей областью (useTranslation("about") + t("counter.label") или useTranslations("about.counter") + t("label")).
    • Расположение файлов

      • next-i18next ожидает JSON в public/locales/{lng}/{ns}.json.
      • next-intl гибок; загружайте сообщения как настроите.
      • Intlayer хранит контент в словарях TS/JS и разрешает по ключу.

    Использование в серверном компоненте

    Мы рассмотрим случай UI-компонента. Этот компонент является серверным компонентом и должен иметь возможность быть вставленным в качестве дочернего компонента клиента. (страница (серверный компонент) -> клиентский компонент -> серверный компонент). Поскольку этот компонент может быть вставлен как дочерний компонент клиента, он не может быть асинхронным.

    src/pages/about.tsx
    import type { GetStaticProps } from "next";
    import { useTranslation } from "next-i18next";
    
    type ServerComponentProps = {
      count: number;
    };
    
    const ServerComponent = ({ count }: ServerComponentProps) => {
      const { t, i18n } = useTranslation("about");
      const formatted = new Intl.NumberFormat(i18n.language).format(count);
    
      return (
        <div>
          <p>{formatted}</p>
          <button aria-label={t("counter.label")}>{t("counter.increment")}</button>
        </div>
      );
    };
    
    src/components/ServerComponent.tsx
    type ServerComponentProps = {
      count: number;
      t: (key: string) => string;
      formatter: Intl.NumberFormat;
    };
    
    const ServerComponent = ({ t, count, formatter }: ServerComponentProps) => {
      const formatted = formatter.format(count);
    
      return (
        <div>
          <p>{formatted}</p>
          <button aria-label={t("label")}>{t("increment")}</button>
        </div>
      );
    };
    

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

    • const t = await getTranslations("about.counter");
    • const formatter = await getFormatter().then((formatter) => formatter.number());
    src/components/ServerComponent.tsx
    import { useIntlayer, useNumber } from "next-intlayer/server";
    
    const ServerComponent = ({ count }: { count: number }) => {
      const { label, increment } = useIntlayer("counter");
      const { number } = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    
    Intlayer предоставляет безопасные для сервера хуки через next-intlayer/server. Для работы useIntlayer и useNumber используют синтаксис, похожий на хуки клиента, но в основе зависят от серверного контекста (IntlayerServerProvider).

    Метаданные / Sitemap / Robots

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

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

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

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

    i18n.config.ts
    export const locales = ["en", "fr"] as const;
    export type Locale = (typeof locales)[number];
    export const defaultLocale: Locale = "en";
    
    export function localizedPath(locale: string, path: string) {
      return locale === defaultLocale ? path : "/" + locale + path;
    }
    
    const ORIGIN = "https://example.com";
    export function abs(locale: string, path: string) {
      return ORIGIN + localizedPath(locale, path);
    }
    
    src/app/[locale]/about/layout.tsx
    import type { Metadata } from "next";
    import { locales, defaultLocale, localizedPath } from "@/i18n.config";
    
    export async function generateMetadata({
      params,
    }: {
      params: { locale: string };
    }): Promise<Metadata> {
      const { locale } = params;
    
      // Динамически импортировать правильный JSON файл
      const messages = (
        await import("@/../public/locales/" + locale + "/about.json")
      ).default;
    
      const languages = Object.fromEntries(
        locales.map((locale) => [locale, localizedPath(locale, "/about")])
      );
    
      return {
        title: messages.title,
        description: messages.description,
        alternates: {
          canonical: localizedPath(locale, "/about"),
          languages: { ...languages, "x-default": "/about" },
        },
      };
    }
    
    export default async function AboutPage() {
      return <h1>О нас</h1>; // Заголовок страницы "О нас"
    }
    
    src/app/sitemap.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale, abs } from "@/i18n.config";
    
    export default function sitemap(): MetadataRoute.Sitemap {
      const languages = Object.fromEntries(
        locales.map((locale) => [locale, abs(locale, "/about")])
      );
      return [
        {
          url: abs(defaultLocale, "/about"),
          lastModified: new Date(), // Дата последнего изменения
          changeFrequency: "monthly", // Частота обновления: ежемесячно
          priority: 0.7, // Приоритет страницы
          alternates: { languages }, // Альтернативные языковые версии
        },
      ];
    }
    
    src/app/robots.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale, localizedPath } from "@/i18n.config";
    
    const ORIGIN = "https://example.com";
    
    // Функция для расширения пути на все локали
    const expandAllLocales = (path: string) => [
      localizedPath(defaultLocale, path),
      ...locales
        .filter((locale) => locale !== defaultLocale)
        .map((locale) => localizedPath(locale, path)),
    ];
    
    export default function robots(): MetadataRoute.Robots {
      // Запрет доступа к определённым путям для всех локалей
      const disallow = [
        ...expandAllLocales("/dashboard"),
        ...expandAllLocales("/admin"),
      ];
    
      return {
        rules: { userAgent: "*", allow: ["/"], disallow },
        host: ORIGIN,
        sitemap: ORIGIN + "/sitemap.xml",
      };
    }
    
    src/app/[locale]/about/layout.tsx
    import type { Metadata } from "next";
    import { locales, defaultLocale } from "@/i18n";
    import { getTranslations } from "next-intl/server";
    
    function localizedPath(locale: string, path: string) {
      return locale === defaultLocale ? path : "/" + locale + path;
    }
    
    export async function generateMetadata({
      params,
    }: {
      params: { locale: string };
    }): Promise<Metadata> {
      const { locale } = params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const url = "/about";
      const languages = Object.fromEntries(
        locales.map((locale) => [locale, localizedPath(locale, url)])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, url),
          languages: { ...languages, "x-default": url },
        },
      };
    }
    
    // ... Остальная часть кода страницы
    
    src/app/sitemap.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale } from "@/i18n";
    
    const origin = "https://example.com";
    
    const formatterLocalizedPath = (locale: string, path: string) =>
      locale === defaultLocale ? origin + path : origin + "/" + locale + path;
    
    export default function sitemap(): MetadataRoute.Sitemap {
      const aboutLanguages = Object.fromEntries(
        locales.map((l) => [l, formatterLocalizedPath(l, "/about")])
      );
    
      return [
        {
          url: formatterLocalizedPath(defaultLocale, "/about"),
          lastModified: new Date(),
          changeFrequency: "monthly",
          priority: 0.7,
          alternates: { languages: aboutLanguages },
        },
      ];
    }
    
    src/app/robots.ts
    import type { MetadataRoute } from "next";
    import { locales, defaultLocale } from "@/i18n";
    
    const origin = "https://example.com";
    const withAllLocales = (path: string) => [
      path,
      ...locales
        .filter((locale) => locale !== defaultLocale)
        .map((locale) => "/" + locale + path),
    ];
    
    export default function robots(): MetadataRoute.Robots {
      const disallow = [
        ...withAllLocales("/dashboard"),
        ...withAllLocales("/admin"),
      ];
    
      return {
        rules: { userAgent: "*", allow: ["/"], disallow },
        host: origin,
        sitemap: origin + "/sitemap.xml",
      };
    }
    
    src/app/[locale]/about/layout.tsx
    import { getIntlayer, getMultilingualUrls } from "intlayer";
    import type { Metadata } from "next";
    import type { LocalPromiseParams } from "next-intlayer";
    
    export const generateMetadata = async ({
      params,
    }: LocalPromiseParams): Promise<Metadata> => {
      const { locale } = await params;
    
      const metadata = getIntlayer("page-metadata", locale);
    
      const multilingualUrls = getMultilingualUrls("/about");
    
      return {
        ...metadata,
        alternates: {
          canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
          languages: { ...multilingualUrls, "x-default": "/about" },
        },
      };
    };
    
    // ... Остальная часть кода страницы
    
    src/app/sitemap.ts
    import { getMultilingualUrls } from "intlayer";
    import type { MetadataRoute } from "next";
    
    const sitemap = (): MetadataRoute.Sitemap => [
      {
        url: "https://example.com/about",
        alternates: {
          languages: { ...getMultilingualUrls("https://example.com/about") },
        },
      },
    ];
    
    src/app/robots.ts
    import { getMultilingualUrls } from "intlayer";
    import type { MetadataRoute } from "next";
    
    // Получить все многоязычные URL из массива URL
    const getAllMultilingualUrls = (urls: string[]) =>
      urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);
    
    // Конфигурация для robots.txt с правилами доступа
    const robots = (): MetadataRoute.Robots => ({
      rules: {
        userAgent: "*",
        allow: ["/"],
        disallow: getAllMultilingualUrls(["/dashboard"]),
      },
      host: "https://example.com",
      sitemap: "https://example.com/sitemap.xml",
    });
    
    export default robots;
    
    Intlayer предоставляет функцию getMultilingualUrls для генерации многоязычных URL для вашей карты сайта.

    Middleware для маршрутизации локалей

    Добавьте middleware для обработки определения локали и маршрутизации:

    src/middleware.ts
    import { NextResponse, type NextRequest } from "next/server";
    import { defaultLocale, locales } from "@/i18n.config";
    
    const PUBLIC_FILE = /\.[^/]+$/; // исключить файлы с расширениями
    
    export function middleware(request: NextRequest) {
      const { pathname } = request.nextUrl;
    
      if (
        pathname.startsWith("/_next") ||
        pathname.startsWith("/api") ||
        pathname.startsWith("/static") ||
        PUBLIC_FILE.test(pathname)
      ) {
        return;
      }
    
      const hasLocale = locales.some(
        (l) => pathname === "/" + l || pathname.startsWith("/" + l + "/")
      );
      if (!hasLocale) {
        const locale = defaultLocale;
        const url = request.nextUrl.clone();
        url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);
        return NextResponse.redirect(url);
      }
    }
    
    export const config = {
      matcher: [
        // Совпадение всех путей, кроме начинающихся с этих и файлов с расширениями
        "/((?!api|_next|static|.*\\..*).*)",
      ],
    };
    

    Добавьте middleware для обработки определения локали и маршрутизации:

    src/middleware.ts
    import createMiddleware from "next-intl/middleware";
    import { locales, defaultLocale } from "@/i18n";
    
    export default createMiddleware({
      locales: [...locales],
      defaultLocale,
      localeDetection: true,
    });
    
    export const config = {
      // Пропустить API, Next внутренние компоненты и статические ассеты
      matcher: ["/((?!api|_next|.*\\..*).*)"],
    };
    

    Intlayer предоставляет встроенную обработку middleware через конфигурацию пакета next-intlayer.

    src/middleware.ts
    import { intlayerProxy } from "next-intlayer/proxy";
    
    export const middleware = intlayerProxy();
    
    // применяет этот middleware только к файлам в директории app
    export const config = {
      matcher: "/((?!api|_next|static|.*\\..*).*)",
    };
    

    Конфигурация middleware централизована в файле intlayer.config.ts.

    Контрольный список настройки и лучшие практики

    • Убедитесь, что lang и dir установлены на корневом <html> в src/app/[locale]/layout.tsx.
    • Разделите переводы на пространства имен (например common.json, about.json) в src/locales/<locale>/.
    • Загружайте только требуемые пространства имен в клиентских компонентах с помощью useTranslation('<ns>') и обозначив область I18nProvider теми же пространствами имен.
    • Держите страницы статичными когда возможно: экспортируйте export const dynamic = 'force-static' на страницах; установите dynamicParams = false и реализуйте generateStaticParams.
    • Используйте синхронные серверные компоненты, вложенные под клиентские границы, передавая предварительно вычисленные строки или функцию t и locale.
    • Для SEO установите alternates.languages в метаданных, укажите локализованные URLs в sitemap.ts и запретите дублирующиеся локализованные маршруты в robots.ts.
    • Предпочитайте форматеры, зависящие от локали (например Intl.NumberFormat(locale)) и мемоизируйте их на клиенте, если используется React < 19.
    • Установите html lang и dir: В src/app/[locale]/layout.tsx вычислите dir через getLocaleDirection(locale) и установите <html lang={locale} dir={dir}>.
    • Разделите сообщения по пространствам имен: Организуйте JSON по локали и пространству имен (например common.json, about.json).
    • Минимизируйте полезную нагрузку клиента: На страницах отправляйте только требуемые пространства имен в NextIntlClientProvider (например pick(messages, ['common', 'about'])).
    • Предпочитайте статичные страницы: Экспортируйте export const dynamic = 'force-static' и генерируйте статичные параметры для всех locales.
    • Синхронные серверные компоненты: Держите серверные компоненты синхронными, передавая предварительно вычисленные строки (переведенные метки, отформатированные числа) вместо асинхронных вызовов или функций, которые нельзя сериализовать.
    • Модульное содержимое: Разместите словари содержимого рядом с компонентами, используя файлы .content.{ts|js|json}.
    • Безопасность типов: Используйте интеграцию TypeScript для проверки содержимого во время компиляции.
    • Оптимизация на этапе сборки: Используйте инструменты сборки Intlayer для автоматического tree-shaking и оптимизации пакета.
    • Встроенные инструменты: Используйте встроенную маршрутизацию, помощники SEO и поддержку визуального редактора.

    И победитель…

    Это не просто. У каждого варианта есть свои компромиссы. Вот как я это вижу:

    next-intl

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

    next-i18next

    • зрелый, полнофункциональный, с множеством плагинов сообщества, но с более высокой стоимостью настройки. Если вам нужна экосистема плагинов i18next (например, расширенные правила ICU через плагины) и ваша команда уже знакома с i18next, принимая больше конфигураций ради гибкости.

    Intlayer

    • создан для современного Next.js, с модульным контентом, типобезопасностью, инструментами и меньшим количеством шаблонного кода. Если вы цените контент, ограниченный компонентом, строгий TypeScript, гарантии на этапе сборки, tree-shaking и встроенные маршрутизацию/SEO/редакторские инструменты, особенно для Next.js App Router, дизайн-систем и больших, модульных кодовых баз.

    Если вы предпочитаете минимальную настройку и готовы к некоторой ручной интеграции, next-intl, хороший выбор. Если вам нужны все функции и вы не боитесь сложности, подойдет next-i18next. Но если вы хотите современное, масштабируемое, модульное решение с встроенными инструментами, Intlayer стремится предоставить это из коробки.

    Альтернатива для корпоративных команд: Если вам нужно проверенное решение, которое отлично работает с устоявшимися платформами локализации, такими как Crowdin, Phrase или другими профессиональными системами управления переводами, рассмотрите next-intl или next-i18next за их зрелую экосистему и проверенные интеграции.
    План развития: Intlayer также планирует разработать плагины, которые будут работать поверх решений i18next и next-intl. Это даст вам преимущества Intlayer в автоматизации, синтаксисе и управлении контентом, сохраняя при этом безопасность и стабильность, обеспечиваемые этими устоявшимися решениями в вашем коде приложения.

    GitHub ЗВЁЗДЫ

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

    График истории звёзд


    Заключение

    Все три библиотеки успешно справляются с основной локализацией. Разница в том, сколько работы вам придется выполнить, чтобы добиться надежной и масштабируемой настройки в современном Next.js:

    • С Intlayer модульный контент, строгий TS, безопасность на этапе сборки, tree-shaken бандлы и первоклассный App Router + инструменты SEO являются стандартом, а не рутиной.
    • Если ваша команда ценит поддерживаемость и скорость в многоязычном приложении, ориентированном на компоненты, Intlayer предлагает сегодня самый полный опыт.

    Обратитесь к документу «Почему Intlayer?» для получения дополнительной информации.

    Комментарии

    Пока нет комментариев. Будьте первым, кто поделится своими мыслями.

    Похожие сообщения

    Последние сообщения