Настройка i18n-фреймворка (next-intl) для Next.js
next-intl — де-факто стандарт для i18n в Next.js App Router. Интегрируется с серверными компонентами (RSC), поддерживает Server Actions, работает со статической генерацией и стриминговым рендером. В отличие от конкурентов, переводы доступны в Server Components без клиентского JS.
Установка
npm install next-intl
Версия 3.x требует Next.js 13.4+ с App Router.
Структура файлов
messages/
ru.json
en.json
de.json
src/
app/
[locale]/
layout.tsx
page.tsx
catalog/
page.tsx
i18n.ts
middleware.ts
Конфигурация
// src/i18n.ts
import { getRequestConfig } from 'next-intl/server'
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`../messages/${locale}.json`)).default,
timeZone: 'Europe/Moscow',
now: new Date(),
}))
// src/middleware.ts
import createMiddleware from 'next-intl/middleware'
export default createMiddleware({
locales: ['ru', 'en', 'de', 'uk'],
defaultLocale: 'ru',
localePrefix: 'as-needed', // /ru/ опускается для defaultLocale
})
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
}
// next.config.js
const withNextIntl = require('next-intl/plugin')('./src/i18n.ts')
module.exports = withNextIntl({
// остальная конфигурация
})
Файлы переводов
// messages/ru.json
{
"nav": {
"catalog": "Каталог",
"cart": "Корзина",
"account": "Личный кабинет"
},
"catalog": {
"title": "Каталог товаров",
"items": "{count, plural, one {# товар} few {# товара} many {# товаров} other {# товаров}}",
"filter": "Фильтры",
"sort": "Сортировка",
"sort_price_asc": "Дешевле",
"sort_price_desc": "Дороже",
"empty": "Товары не найдены"
},
"product": {
"addToCart": "В корзину",
"buyNow": "Купить сейчас",
"inStock": "В наличии",
"outOfStock": "Нет в наличии"
}
}
Layout с провайдером
// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl'
import { getMessages, getTranslations } from 'next-intl/server'
import { notFound } from 'next/navigation'
const locales = ['ru', 'en', 'de', 'uk']
export async function generateStaticParams() {
return locales.map(locale => ({ locale }))
}
export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'meta' })
return {
title: t('title'),
description: t('description'),
}
}
export default async function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode
params: { locale: string }
}) {
if (!locales.includes(locale)) notFound()
// Передаём сообщения в Client Components
const messages = await getMessages()
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
Использование в Server Components
// src/app/[locale]/catalog/page.tsx
import { getTranslations } from 'next-intl/server'
export default async function CatalogPage({
params: { locale },
searchParams,
}: {
params: { locale: string }
searchParams: { page?: string }
}) {
const t = await getTranslations('catalog')
const products = await fetchProducts({ locale, page: Number(searchParams.page ?? 1) })
return (
<main>
<h1>{t('title')}</h1>
<p>{t('items', { count: products.total })}</p>
{/* ... */}
</main>
)
}
Использование в Client Components
'use client'
import { useTranslations, useLocale, useFormatter } from 'next-intl'
function ProductCard({ product }: { product: Product }) {
const t = useTranslations('product')
const locale = useLocale()
const format = useFormatter()
return (
<div>
<h2>{product.title}</h2>
<p>
{format.number(product.price, {
style: 'currency',
currency: locale === 'ru' ? 'RUB' : 'USD',
maximumFractionDigits: 0,
})}
</p>
<span>{product.inStock ? t('inStock') : t('outOfStock')}</span>
<button>{t('addToCart')}</button>
</div>
)
}
Локализованные маршруты (pathnames)
// src/navigation.ts
import { createLocalizedPathnamesNavigation } from 'next-intl/navigation'
export const { Link, redirect, usePathname, useRouter } =
createLocalizedPathnamesNavigation({
locales: ['ru', 'en', 'de'],
pathnames: {
'/': '/',
'/catalog': { ru: '/catalog', en: '/catalog', de: '/katalog' },
'/catalog/[slug]': {
ru: '/catalog/[slug]',
en: '/catalog/[slug]',
de: '/katalog/[slug]',
},
'/checkout': { ru: '/checkout', en: '/checkout', de: '/kasse' },
},
})
// Используем локализованный Link вместо next/link
import { Link } from '@/navigation'
<Link href="/catalog">Каталог</Link>
// Для ru: /catalog
// Для de: /de/katalog
Статическая генерация с переводами
// Генерация статических страниц для всех языков
export async function generateStaticParams() {
const products = await fetchAllProductSlugs()
return ['ru', 'en', 'de'].flatMap(locale =>
products.map(product => ({
locale,
slug: product.slugs[locale],
}))
)
}
TypeScript: автодополнение ключей переводов
// global.d.ts
import ru from './messages/ru.json'
declare module 'next-intl' {
interface AppConfig {
Messages: typeof ru
}
}
Теперь t('nonexistent.key') — ошибка TypeScript.
Сроки
Базовая настройка next-intl с 2–3 языками — 1 день. С локализованными pathname, статической генерацией всех языковых версий и TypeScript-типизацией ключей — 2–3 дня.







