---
createdAt: 2025-08-23
updatedAt: 2025-09-29
title: next-i18next против next-intl против Intlayer
description: Сравнение next-i18next с next-intl и Intlayer для интернационализации (i18n) приложения Next.js
keywords:
- next-intl
- next-i18next
- Intlayer
- Интернационализация
- Блог
- Next.js
- JavaScript
- React
slugs:
- blog
- next-i18next-vs-next-intl-vs-intlayer
---
# next-i18next ПРОТИВ next-intl ПРОТИВ intlayer | Интернационализация (i18n) Next.js
<TOC/>
Давайте рассмотрим сходства и различия между тремя вариантами i18n для Next.js: next-i18next, next-intl и Intlayer.
Это не полный учебник. Это сравнение, чтобы помочь вам сделать выбор.
Мы сосредотачиваемся на **Next.js 13+ App Router** (с **React 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](https://github.com/amannn), а `react-intl` поддерживается [FormatJS](https://github.com/formatjs/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` | [](https://github.com/aymericzip/intlayer/stargazers) | [](https://github.com/aymericzip/intlayer/commits) | [](https://github.com/aymericzip/intlayer/commits) | April 2024 | [](https://www.npmjs.com/package/intlayer) | [](https://www.npmjs.com/package/intlayer) |
| `amannn/next-intl` | [](https://github.com/amannn/next-intl/stargazers) | [](https://github.com/amannn/next-intl/commits) | [](https://github.com/amannn/next-intl/commits) | Nov 2020 | [](https://www.npmjs.com/package/next-intl) | [](https://www.npmjs.com/package/next-intl) |
| `i18next/i18next` | [](https://github.com/i18next/i18next/stargazers) | [](https://github.com/i18next/i18next/commits) | [](https://github.com/i18next/i18next/commits) | Jan 2012 | [](https://www.npmjs.com/package/i18next) | [](https://www.npmjs.com/package/i18next) |
| `i18next/next-i18next` | [](https://github.com/i18next/next-i18next/stargazers) | [](https://github.com/i18next/next-i18next/commits) | [](https://github.com/i18next/next-i18next/commits) | Nov 2018 | [](https://www.npmjs.com/package/next-i18next) | [](https://www.npmjs.com/package/next-i18next) |
> Значки обновляются автоматически. Снимки будут меняться со временем.
---
## Сравнение функций бок о бок (с фокусом на Next.js)
| Функция | `next-intlayer` (Intlayer) | `next-intl` | `next-i18next` |
> Значки обновляются автоматически. Снимки будут меняться со временем.
---
## Сравнение функций бок о бок (с акцентом на 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 и безопасность
<Columns>
<Column>
**next-intl**
- Надёжная поддержка TypeScript, но **ключи по умолчанию не строго типизированы**; вам придётся вручную поддерживать паттерны безопасности.
</Column>
<Column>
**next-i18next**
- Базовые типы для хуков; **строгая типизация ключей требует дополнительного инструментария/конфигурации**.
</Column>
<Column>
**intlayer**
- **Генерирует строгие типы** на основе вашего контента. **Автодополнение в IDE** и **ошибки на этапе компиляции** ловят опечатки и отсутствующие ключи до деплоя.
</Column>
</Columns>
**Почему это важно:** Строгая типизация сдвигает ошибки **влево** (CI/сборка), а не **вправо** (во время выполнения).
---
## Обработка отсутствующих переводов
**next-intl**
- Опирается на **запасные варианты во время выполнения** (например, показывает ключ или локаль по умолчанию). Сборка не прерывается.
**next-i18next**
- Опирается на **запасные варианты во время выполнения** (например, показывает ключ или локаль по умолчанию). Сборка не прерывается.
**intlayer**
- **Обнаружение на этапе сборки** с **предупреждениями/ошибками** для отсутствующих локалей или ключей.
**Почему это важно:** Обнаружение пробелов во время сборки предотвращает появление «непонятных строк» в продакшене и соответствует строгим требованиям к выпуску.
---
## Маршрутизация, middleware и стратегия URL
<Columns>
<Column>
**next-intl**
- Работает с **локализованной маршрутизацией Next.js** на App Router.
</Column>
<Column>
**next-i18next**
- Работает с **локализованной маршрутизацией Next.js** на App Router.
</Column>
<Column>
**intlayer**
- Всё вышеперечисленное, плюс **i18n middleware** (определение локали через заголовки/куки) и **хелперы** для генерации локализованных URL и тегов `<link rel="alternate" hreflang="…">`.
</Column>
</Columns>
**Почему это важно:** Меньше пользовательских прослоек; **последовательный UX** и **чистое SEO** для всех локалей.
---
## Совместимость с серверными компонентами (RSC)
<Columns>
<Column>
**next-intl**
- Поддерживает Next.js 13+. Часто требует передачи t-функций/форматтеров через дерево компонентов в гибридных настройках.
</Column>
<Column>
**next-i18next**
- Поддерживает Next.js 13+. Аналогичные ограничения при передаче утилит перевода через границы.
</Column>
<Column>
**intlayer**
- Поддерживает Next.js 13+ и сглаживает **границу сервер/клиент** с помощью единого API и провайдеров, ориентированных на RSC, избегая передачи форматтеров или функций t.
</Column>
</Columns>
**Почему это важно:** Более чистая ментальная модель и меньше крайних случаев в гибридных деревьях.
---
## DX, инструменты и сопровождение
<Columns>
<Column>
**next-intl**
- Обычно используется вместе с внешними платформами локализации и редакционными рабочими процессами.
</Column>
<Column>
**next-i18next**
- Обычно используется вместе с внешними платформами локализации и редакционными рабочими процессами.
</Column>
<Column>
**intlayer**
- Поставляется с **бесплатным Визуальным Редактором** и **опциональной CMS** (дружелюбной к Git или внешней), а также с **расширением для VSCode** и **переводами с помощью ИИ**, использующими ваши собственные ключи провайдера.
</Column>
</Columns>
**Почему это важно:** Снижает операционные затраты и сокращает цикл взаимодействия между разработчиками и авторами контента.
## Интеграция с платформами локализации (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.
## Опыт разработчика
В этой части проводится глубокое сравнение трех решений. Вместо рассмотрения простых случаев, описанных в документации "начало работы" для каждого решения, мы рассмотрим реальный кейс, более похожий на настоящий проект.
### Структура приложения
Структура приложения важна для обеспечения хорошей поддерживаемости вашего кода.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
```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
```
</TabItem>
<TabItem label="next-intl" value="next-intl">
```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
```
</TabItem>
<TabItem label="intlayer" value="intlayer">
```bash
.
├── intlayer.config.ts
└── src
├── middleware.ts
├── app
│ └── [locale]
│ ├── layout.tsx
│ └── home
│ ├── index.tsx
│ └── index.content.ts
└── components
└── Navbar
├── index.tsx
└── index.content.ts
```
</TabItem>
</Tab>
#### Сравнение
- **next-intl / next-i18next**: Централизованные каталоги (JSON; пространства имён/сообщения). Чёткая структура, хорошо интегрируется с платформами перевода, но может привести к большему количеству изменений в разных файлах по мере роста приложений.
- **Intlayer**: Словари `.content.{ts|js|json}` для каждого компонента, расположенные рядом с компонентами. Облегчает повторное использование компонентов и локальное понимание; добавляет файлы и опирается на инструменты сборки.
#### Настройка и загрузка контента
Как упоминалось ранее, вы должны оптимизировать способ импорта каждого JSON-файла в ваш код.
Важен способ, которым библиотека обрабатывает загрузку контента.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
```tsx fileName="next-i18next.config.js"
module.exports = {
i18n: {
locales: ["en", "fr", "es"],
defaultLocale: "en",
},
};
```
```tsx fileName="src/app/_app.tsx"
import { appWithTranslation } from "next-i18next";
const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;
export default appWithTranslation(MyApp);
```
```tsx fileName="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"])),
},
};
};
```
</TabItem>
<TabItem label="next-intl" value="next-intl">
```tsx fileName="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 as any)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default,
};
});
```
```tsx fileName="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>
);
}
```
```tsx fileName="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>
);
}
```
</TabItem>
<TabItem label="intlayer" value="intlayer">
```tsx fileName="intlayer.config.ts"
export default {
internationalization: {
locales: ["en", "fr", "es"],
defaultLocale: "en",
},
};
```
```tsx fileName="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;
```
```tsx fileName="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;
```
</TabItem>
</Tab>
#### Сравнение
Все три решения поддерживают загрузку контента и провайдеры для каждой локали.
- С **next-intl/next-i18next** вы обычно загружаете выбранные сообщения/пространства имён для каждого маршрута и размещаете провайдеры там, где это необходимо.
- С **Intlayer** добавляется анализ во время сборки для определения использования, что может сократить ручное подключение и позволить использовать один корневой провайдер.
Выбирайте между явным контролем и автоматизацией в зависимости от предпочтений вашей команды.
### Использование в клиентском компоненте
Рассмотрим пример клиентского компонента, который отображает счётчик.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
**Переводы (должны быть настоящим JSON в `public/locales/...`)**
```json fileName="public/locales/en/about.json"
{
"counter": {
"label": "Счётчик",
"increment": "Увеличить"
}
}
```
```json fileName="public/locales/fr/about.json"
{
"counter": {
"label": "Счётчик",
"increment": "Увеличить"
}
}
```
**Клиентский компонент**
```tsx fileName="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 для хранения экземпляра форматтера, так как это тяжёлая функция
</TabItem>
<TabItem label="next-intl" value="next-intl">
**Переводы (используется та же структура; загружайте их в сообщения next-intl по своему усмотрению)**
```json fileName="locales/en/about.json"
{
"counter": {
"label": "Counter",
"increment": "Increment"
}
}
```
```json fileName="locales/fr/about.json"
{
"counter": {
"label": "Compteur",
"increment": "Incrémenter"
}
}
```
**Клиентский компонент**
```tsx fileName="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" в клиентские сообщения страницы
</TabItem>
<TabItem label="intlayer" value="intlayer">
**Содержание**
```ts fileName="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;
```
**Клиентский компонент**
```tsx fileName="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>
);
};
```
</TabItem>
</Tab>
#### Сравнение
- **Форматирование чисел**
- **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-компонента. Этот компонент является серверным компонентом и должен иметь возможность быть вставленным в качестве дочернего компонента клиента. (страница (серверный компонент) -> клиентский компонент -> серверный компонент). Поскольку этот компонент может быть вставлен как дочерний компонент клиента, он не может быть асинхронным.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
```tsx fileName="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>
);
};
```
</TabItem>
<TabItem label="next-intl" value="next-intl">
```tsx fileName="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());`
</TabItem>
<TabItem label="intlayer" value="intlayer">
```tsx fileName="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>
);
};
```
</TabItem>
</Tab>
> 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>`)
>
Разработчики часто забывают правильно ссылаться на свои страницы в разных локалях.
<Tab defaultTab="next-intl" group='techno'>
<TabItem label="next-i18next" value="next-i18next">
```ts fileName="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);
}
```
```tsx fileName="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>; // Заголовок страницы "О нас"
}
```
```ts fileName="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 }, // Альтернативные языковые версии
},
];
}
```
```ts fileName="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",
};
}
```
</TabItem>
<TabItem label="next-intl" value="next-intl">
```tsx fileName="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 },
},
};
}
// ... Остальная часть кода страницы
```
```tsx fileName="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 },
},
];
}
```
```tsx fileName="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",
};
}
```
</TabItem>
<TabItem label="intlayer" value="intlayer">
```typescript fileName="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" },
},
};
};
// ... Остальная часть кода страницы
```
```tsx fileName="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") },
},
},
];
```
```tsx fileName="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;
```
</TabItem>
</Tab>
> Intlayer предоставляет функцию `getMultilingualUrls` для генерации многоязычных URL для вашей карты сайта.
---
---
## И победитель…
Это не просто. У каждого варианта есть свои компромиссы. Вот как я это вижу:
<Columns>
<Column>
**next-intl**
- самый простой, легковесный, с меньшим количеством навязанных решений. Если вы хотите **минимальное** решение, вам комфортно с централизованными каталогами, и ваше приложение **маленькое или среднего размера**.
</Column>
<Column>
**next-i18next**
- зрелый, полнофункциональный, с множеством плагинов сообщества, но с более высокой стоимостью настройки. Если вам нужна **экосистема плагинов i18next** (например, расширенные правила ICU через плагины) и ваша команда уже знакома с i18next, принимая **больше конфигураций** ради гибкости.
</Column>
<Column>
**Intlayer**
- создан для современного Next.js, с модульным контентом, типобезопасностью, инструментами и меньшим количеством шаблонного кода. Если вы цените **контент, ограниченный компонентом**, **строгий TypeScript**, **гарантии на этапе сборки**, **tree-shaking** и **встроенные** маршрутизацию/SEO/редакторские инструменты — особенно для **Next.js App Router**, дизайн-систем и **больших, модульных кодовых баз**.
</Column>
</Columns>
Если вы предпочитаете минимальную настройку и готовы к некоторой ручной интеграции, next-intl — хороший выбор. Если вам нужны все функции и вы не боитесь сложности, подойдет next-i18next. Но если вы хотите современное, масштабируемое, модульное решение с встроенными инструментами, Intlayer стремится предоставить это из коробки.
> **Альтернатива для корпоративных команд**: Если вам нужно проверенное решение, которое отлично работает с устоявшимися платформами локализации, такими как **Crowdin**, **Phrase** или другими профессиональными системами управления переводами, рассмотрите **next-intl** или **next-i18next** за их зрелую экосистему и проверенные интеграции.
> **План развития**: Intlayer также планирует разработать плагины, которые будут работать поверх решений **i18next** и **next-intl**. Это даст вам преимущества Intlayer в автоматизации, синтаксисе и управлении контентом, сохраняя при этом безопасность и стабильность, обеспечиваемые этими устоявшимися решениями в вашем коде приложения.
## GitHub ЗВЁЗДЫ
Звёзды на GitHub являются сильным индикатором популярности проекта, доверия сообщества и его долгосрочной актуальности. Хотя они не являются прямой мерой технического качества, они отражают, сколько разработчиков считают проект полезным, следят за его развитием и, вероятно, будут его использовать. Для оценки ценности проекта звёзды помогают сравнивать популярность среди альтернатив и дают представление о росте экосистемы.
[](https://www.star-history.com/#i18next/next-i18next&amannn/next-intl&aymericzip/intlayer)
---
## Заключение
Все три библиотеки успешно справляются с основной локализацией. Разница в том, **сколько работы вам придется выполнить**, чтобы добиться надежной и масштабируемой настройки в **современном Next.js**:
- С **Intlayer** **модульный контент**, **строгий TS**, **безопасность на этапе сборки**, **tree-shaken бандлы** и **первоклассный App Router + инструменты SEO** являются **стандартом**, а не рутиной.
- Если ваша команда ценит **поддерживаемость и скорость** в многоязычном приложении, ориентированном на компоненты, Intlayer предлагает сегодня **самый полный** опыт.
Обратитесь к [документу «Почему Intlayer?»](https://intlayer.org/doc/why) для получения дополнительной информации.