Next.js App Router i18n: Сравнение библиотек и выбор next-intl
Подробное сравнение i18n решений для Next.js 15+: next-intl, next-i18next, next-translate и custom решения. Почему next-intl — лучший выбор для App Router.
Проблема
Вы хотите добавить поддержку нескольких языков в Next.js приложение с App Router. Открываете поиск и видите десятки решений:
next-intlnext-i18nextnext-translate- "Напишите свое решение"
- "Используйте встроенный i18n Next.js"
Какое выбрать? Почему одни рекомендуют next-i18next, а другие говорят, что это устаревшее решение? Давайте разберемся системно.
Реальная задача
Недавно я внедрял локализацию в проект с такими требованиями:
- Next.js 15+ с App Router
- Русский (основной) и Английский
- URL структура:
/ru/blog,/en/blog(всегда с префиксом локали) - SEO: правильные hreflang теги, sitemap с локалями
- Server Components для производительности
- Простота поддержки и расширения
Звучит стандартно, но выбор i18n решения оказался не таким очевидным.
Сравнительная таблица
Я составил таблицу на основе критериев, важных для современного Next.js проекта:
| Критерий | next-intl | next-i18next | next-translate | Custom | Вес |
|---|---|---|---|---|---|
| App Router support | ✅ Отличная | ⚠️ Ограниченная | ❌ Только Pages | ✅ Полный контроль | 30% |
| Performance | ✅ Server Components | ⚠️ Client-side | ⚠️ Client-side | ✅ Оптимизированная | 25% |
| Developer Experience | ✅ Отличный | ✅ Хороший | ⚠️ Средний | ❌ Сложный | 20% |
| Bundle size | ✅ Маленький (~5KB) | ⚠️ Средний (~15KB) | ✅ Маленький (~3KB) | ✅ Минимальный | 15% |
| Функционал | ✅ Богатый | ✅ Богатый | ⚠️ Базовый | ❌ Нужно писать | 10% |
| ИТОГО | 9.5/10 | 6.5/10 | 5/10 | 6/10 | 100% |
Разбор по критериям
1. App Router Support (30% веса)
next-intl: ✅
- Разработан специально для App Router
- Поддержка Server Components из коробки
- Middleware для роутинга
- Активная разработка и обновления
next-i18next: ⚠️
- Изначально для Pages Router
- Поддержка App Router "через костыли"
- Документация в основном для Pages
- Разработчики сами рекомендуют
next-intlдля App Router
next-translate: ❌
- Официально поддерживает только Pages Router
- App Router вообще не в планах
Custom: ✅
- Полный контроль, но...
- Нужно реализовать всё самому
- Много edge cases
2. Performance (25% веса)
next-intl: ✅
// Server Component - переводы загружаются на сервере
import { getTranslations } from 'next-intl/server'
export default async function BlogPage() {
const t = await getTranslations('Blog')
return <h1>{t('title')}</h1> // Рендерится на сервере!
}
next-i18next: ⚠️
// Client Component - переводы в bundle
'use client'
import { useTranslation } from 'next-i18next'
export default function BlogPage() {
const { t } = useTranslation('blog')
return <h1>{t('title')}</h1> // Client-side рендеринг
}
Результат:
next-intl: переводы не попадают в JS bundle → меньше bundle, быстрее FCPnext-i18next: все переводы в bundle → больше bundle, медленнее загрузка
3. Developer Experience (20% веса)
next-intl: ✅
// messages/ru.json
{
"Blog": {
"title": "Блог",
"readMore": "Читать далее"
}
}
// components/BlogCard.tsx
import { useTranslations } from 'next-intl'
export default function BlogCard() {
const t = useTranslations('Blog')
return (
<div>
<h2>{t('title')}</h2>
<button>{t('readMore')}</button>
</div>
)
}
TypeScript автодополнение:
// Можно сгенерировать типы из JSON
type Messages = typeof import('./messages/ru.json')
declare global {
interface IntlMessages extends Messages {}
}
next-i18next: ✅ (похоже, но без Server Components)
next-translate: ⚠️ (меньше возможностей)
Custom: ❌ (всё нужно писать самому)
4. Bundle Size (15% веса)
Реальные замеры для типичного проекта:
# next-intl
Client bundles:
├─ /ru/blog ~125 KB (gzip: ~35 KB)
└─ i18n overhead: ~5 KB
# next-i18next
Client bundles:
├─ /ru/blog ~150 KB (gzip: ~42 KB)
└─ i18n overhead: ~15 KB + все переводы страницы
Вывод: next-intl экономит ~7-10 KB на странице благодаря Server Components.
5. Функционал (10% веса)
Что нужно в реальных проектах:
| Функция | next-intl | next-i18next | next-translate | Custom |
|---|---|---|---|---|
| Базовые переводы | ✅ | ✅ | ✅ | 🔨 |
| Плюрализация | ✅ | ✅ | ✅ | 🔨 |
| Форматирование дат | ✅ | ✅ | ⚠️ | 🔨 |
| Форматирование чисел | ✅ | ✅ | ⚠️ | 🔨 |
| RTL поддержка | ✅ | ✅ | ❌ | 🔨 |
| Локализованные URL | ✅ | ✅ | ⚠️ | 🔨 |
| Metadata (SEO) | ✅ | ⚠️ | ❌ | 🔨 |
| Time zones | ✅ | ⚠️ | ❌ | 🔨 |
= нужно писать самому
Выбор: next-intl
Почему именно next-intl?
- Создан для App Router — не нужны хаки и костыли
- Server Components — лучшая производительность из коробки
- Отличный DX — типизация, автодополнение, понятная документация
- Активная разработка — обновления под каждую версию Next.js
- Богатый функционал — всё, что нужно для enterprise проектов
Когда НЕ выбирать next-intl?
- Используете Pages Router →
next-i18next - Миграция с
next-i18nextи нет времени переписывать → оставьтеnext-i18next - Очень простой проект на 2 страницы → может хватить и custom решения
Практическая реализация
Установка
npm install next-intl
Структура проекта
app/
├── [locale]/
│ ├── layout.tsx # Layout с NextIntlClientProvider
│ ├── page.tsx # Главная страница
│ ├── blog/
│ │ ├── page.tsx
│ │ └── [slug]/page.tsx
│ └── about/
│ └── page.tsx
├── layout.tsx # Root layout
└── page.tsx # Redirect на /ru
messages/
├── ru.json # Русские переводы
└── en.json # Английские переводы
i18n/
├── config.ts # Конфигурация локалей
├── request.ts # Загрузка сообщений
└── routing.ts # Роутинг и навигация
middleware.ts # Middleware для локалей
Конфигурация
1. i18n/config.ts
export const locales = ['ru', 'en'] as const
export type Locale = (typeof locales)[number]
export const defaultLocale: Locale = 'ru'
export const localeNames: Record<Locale, string> = {
ru: 'Русский',
en: 'English',
}
export const localeFlags: Record<Locale, string> = {
ru: '🇷🇺',
en: '🇬🇧',
}
2. i18n/routing.ts
import { defineRouting } from 'next-intl/routing'
import { locales, defaultLocale } from './config'
export const routing = defineRouting({
locales,
defaultLocale,
localePrefix: 'always', // Всегда показываем локаль в URL
})
export const { Link, redirect, usePathname, useRouter } =
createNavigation(routing)
3. i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
import { routing } from './routing'
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
}
})
4. middleware.ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'
export default createMiddleware(routing)
export const config = {
matcher: ['/', '/(ru|en)/:path*'],
}
Файлы переводов
messages/ru.json
{
"Navigation": {
"home": "Главная",
"blog": "Блог",
"about": "О нас"
},
"Blog": {
"title": "Блог",
"readMore": "Читать далее",
"publishedAt": "Опубликовано {date}",
"readingTime": "{minutes} мин. чтения"
},
"SEO": {
"title": "DevBlog — Web Development Articles",
"description": "Tutorials about Next.js, React, TypeScript and modern web development"
}
}
messages/en.json
{
"Navigation": {
"home": "Home",
"blog": "Blog",
"about": "About"
},
"Blog": {
"title": "Blog",
"readMore": "Read more",
"publishedAt": "Published on {date}",
"readingTime": "{minutes} min read"
},
"SEO": {
"title": "DevBlog — Web Development Articles",
"description": "Tutorials about Next.js, React and TypeScript"
}
}
Использование в компонентах
Server Component
// app/[locale]/blog/page.tsx
import { getTranslations } from 'next-intl/server'
export async function generateMetadata({
params: { locale },
}: {
params: { locale: string }
}) {
const t = await getTranslations({ locale, namespace: 'SEO' })
return {
title: t('title'),
description: t('description'),
}
}
export default async function BlogPage() {
const t = await getTranslations('Blog')
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
)
}
Client Component
'use client'
import { useTranslations } from 'next-intl'
export default function BlogCard({ publishedAt }: { publishedAt: Date }) {
const t = useTranslations('Blog')
return (
<div>
<button>{t('readMore')}</button>
<time>
{t('publishedAt', { date: publishedAt.toLocaleDateString() })}
</time>
</div>
)
}
Переключатель языков
'use client'
import { useParams } from 'next/navigation'
import { usePathname, useRouter } from '@/i18n/routing'
import { locales, localeFlags, localeNames } from '@/i18n/config'
export default function LanguageSwitcher() {
const params = useParams()
const pathname = usePathname()
const router = useRouter()
const currentLocale = params.locale as string
const switchLocale = (locale: string) => {
router.replace(pathname, { locale })
}
return (
<div className="flex gap-2">
{locales.map((locale) => (
<button
key={locale}
onClick={() => switchLocale(locale)}
className={currentLocale === locale ? 'font-bold' : ''}
>
{localeFlags[locale]} {localeNames[locale]}
</button>
))}
</div>
)
}
Продвинутые возможности
Форматирование дат и чисел
import { useFormatter } from 'next-intl'
export default function StatsCard() {
const format = useFormatter()
const views = 1_234_567
const date = new Date('2025-01-15')
return (
<div>
<p>{format.number(views)}</p> {/* 1 234 567 или 1,234,567 */}
<time>{format.dateTime(date, { dateStyle: 'long' })}</time>
</div>
)
}
SEO: Hreflang теги
// app/[locale]/blog/[slug]/page.tsx
export async function generateMetadata({
params: { locale, slug },
}: PageProps) {
const t = await getTranslations({ locale, namespace: 'SEO' })
return {
title: t('blogPost.title'),
description: t('blogPost.description'),
alternates: {
canonical: `/${locale}/blog/${slug}`,
languages: {
ru: `/ru/blog/${slug}`,
en: `/en/blog/${slug}`,
},
},
}
}
Результат в HTML:
<link rel="canonical" href="https://example.com/ru/blog/nextjs-i18n" />
<link rel="alternate" hreflang="ru" href="https://example.com/ru/blog/nextjs-i18n" />
<link rel="alternate" hreflang="en" href="https://example.com/en/blog/nextjs-i18n" />
<link rel="alternate" hreflang="x-default" href="https://example.com/ru/blog/nextjs-i18n" />
4. Локализованный Sitemap
// app/sitemap.ts
import { getTranslations } from 'next-intl/server'
import { locales } from '@/i18n/config'
export default async function sitemap() {
export default async function sitemap() {
const posts = await getPosts()
return posts.flatMap((post) =>
locales.map((locale) => ({
url: `https://example.com/${locale}/blog/${post.slug}`,
lastModified: post.updatedAt,
}))
)
}
}
Типизация
// types/i18n.d.ts
type Messages = typeof import('../messages/ru.json')
declare global {
interface IntlMessages extends Messages {}
}
Теперь TypeScript будет проверять ключи переводов!
Результаты внедрения
После внедрения next-intl в реальном проекте:
Производительность:
- Bundle уменьшился на ~8 KB (gzip)
- FCP улучшился на ~150ms (переводы на сервере)
- Lighthouse Score: +3 балла
DX:
- TypeScript автодополнение работает отлично
- Hot Reload при изменении переводов
- Понятные error messages
SEO:
- Правильные hreflang теги
- Локализованный sitemap
- Canonical URLs для каждой локали
Подводные камни
1. Server vs Client Components
Ошибка:
// Server Component
import { useTranslations } from 'next-intl' // Это для клиента!
export default async function Page() {
const t = useTranslations('Blog') // Ошибка!
return <h1>{t('title')}</h1>
}
Правильно:
import { getTranslations } from 'next-intl/server'
export default async function Page() {
const t = await getTranslations('Blog')
return <h1>{t('title')}</h1>
}
2. Динамические параметры в Next.js 15
// Всегда await params в Next.js 15
export async function generateMetadata({ params }: PageProps) {
const { locale } = await params
const t = await getTranslations({ locale, namespace: 'SEO' })
// ...
}
Выводы
Когда использовать next-intl:
- Next.js 13+ с App Router
- Нужны Server Components
- Важна производительность
- Сложная локализация (форматирование, плюрализация, etc)
- Enterprise проект
Когда рассмотреть альтернативы:
- Pages Router →
next-i18next - Очень простой проект → Custom решение
- Уже используете
next-i18next→ может не стоит мигрировать
Ключевые преимущества next-intl:
- Server Components — переводы не в bundle
- Типизация — автодополнение в IDE
- Производительность — меньше JS, быстрее FCP
- SEO — правильные hreflang и alternates
- DX — понятный API, хорошая документация
Полезные ссылки
Итог
Выбор i18n решения — это не просто "какая библиотека популярнее". Это анализ:
- Архитектуры вашего приложения (Pages vs App Router)
- Требований к производительности
- Сложности локализации
- Developer Experience
Для современных Next.js проектов с App Router next-intl — это очевидный выбор. Он создан специально для новой архитектуры, использует все её преимущества и предоставляет отличный DX.
Не тратьте время на велосипеды или устаревшие решения. Используйте правильный инструмент для правильной задачи.