В классических Single Page Applications (SPA) интернационализация традиционно сопровождалась существенным ухудшением метрик производительности: клиентский бандл загружал многомегабайтные JSON-словари для всех поддерживаемых языков, а вычисление переводов и форматирование строк происходило в основном потоке браузера (Main Thread), ухудшая показатели Interaction to Next Paint (INP) и Time to Interactive (TTI).
С появлением архитектуры React Server Components (RSC) в Next.js 16 парадигма интернационализации (i18n) претерпела фундаментальный сдвиг. Переводы теперь вычисляются непосредственно на сервере в процессе потокового рендеринга (Streaming SSR). Клиентский браузер получает чистый HTML и сериализованный RSC Flight Payload с уже подставленными текстовыми строками. Стоимость клиентского бандла для серверных переводов равна 0 КБ.
Next.js 16 формализует этот подход через интеграцию библиотеки next-intl, механизм асинхронных параметров сегментов params: Promise<{ locale: string }>, а также API rootParams для детерминированного связывания корневых макетов с региональными локалями.
Ментальная модель современной интернационализации строится на концепции сегментарной изоляции словарей и потоковой интерполяции.
Вместо монолитного словаря приложение декомпозирует языковые ресурсы на гранулярные модули: общие системные ключи (common.json), модули аутентификации (auth.json), страницы каталога (catalog.json).
Входящий HTTP-запрос (/de/checkout):
│
▼
┌────────────────────────────────────────────────────────┐
│ Edge Middleware / i18n Routing Layer │
│ 1. Анализ Accept-Language & Cookie (NEXT_LOCALE) │
│ 2. Определение локали: 'de' │
│ 3. Валидация в белом списке: ['en', 'de', 'fr', 'es'] │
└────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ Server Components Lifecycle (RSC + next-intl) │
│ 1. setRequestLocale('de') -> фиксация контекста │
│ 2. getTranslations({ locale: 'de', namespace }) │
│ 3. Динамический импорт словаря: messages/de.json │
│ 4. Серверная ICU-интерполяция (валюта EUR, плюрал) │
└────────────────────────────────────────────────────────┘
│
▼
Вывод в браузер:
- Готовый HTML: "Gesamtsumme: 1.499,00 €"
- Клиентский JS-бандл: 0 КБ библиотек перевода!Для интерактивных клиентских островов (Client Components) используется селективная передача контекста через провайдер <NextIntlClientProvider messages={scopedMessages}>, куда передаются строго те ключи, которые требуются интерактивным элементам (например, валидация полей ввода формы).
Архитектура разделения словарей между RSC и Client
[Запрос к странице /es/products/42]
│
▼
┌──────────────────────────┐
│ app/[locale]/page.tsx │
│ (React Server Component)│
└────────────┬─────────────┘
│
┌───────────────────────┴───────────────────────┐
│ │
Серверные переводы (RSC) Клиентские переводы (Client)
- Каталог товаров - Интерактивная корзина
- Описание и характеристики - Валидация ввода количества
- SEO-метаданные & hreflang - Модальные уведомления
│ │
▼ ▼
getTranslations('Catalog') <NextIntlClientProvider
Словарь остается на сервере! messages={{ Cart: ... }}>
В бандл клиента: 0 КБ JS В бандл клиента: < 2 КБ JSONMiddleware перехватывает запрос, сопоставляет Accept-Language и куку NEXT_LOCALE с массивом поддерживаемых языков (locales) и выполняет мягкий редирект или rewrite.
Серверный корневой макет app/[locale]/layout.tsx вызывает setRequestLocale(locale), обеспечивая корректную генерацию статических страниц (SSG/ISR).
Модуль getRequestConfig динамически импортирует messages/${locale}.json через асинхронный импорт с автоматической мемоизацией в рамках одного запроса.
Сервер форматирует строки с учетом грамматических правил языка: плюрализация (cardinal/ordinal), форматирование дат через Intl.DateTimeFormat и валют через Intl.NumberFormat.
Функция generateMetadata генерирует канонические ссылки и атрибуты alternates.languages для всех локалей, исключая каннибализацию поисковой выдачи.
В чем главное архитектурное преимущество использования getTranslations() в Server Components по сравнению с традиционным useTranslations() внутри Client Components?
Торговые площадки (Германия, Япония, США, Бразилия) требуют строгого соблюдения локальных форматов цен, налогов и правил отображения скидок. Например, в Германии суммы форматируются с запятой в качестве десятичного разделителя (1.299,99 €), а в США — с точкой ($1,299.99). Серверное форматирование гарантирует отсутствие скачков цен при гидратации.
Порталы технической документации (AWS, Stripe, Vercel) переводятся на 15+ языков. Использование серверного стриминга позволяет мгновенно отдавать первый экран (LCP менее 1.2 с) без необходимости скачивания гигантских словарей терминов клиентом.
Банковские выписки, кредитные калькуляторы и графики платежей требуют строгой плюрализации числительных (1 день, 2 дня, 5 дней в русском языке против 1 day, 2 days в английском). Механизм ICU MessageFormat предотвращает грамматические ошибки в юридических документах.
Крупные издания индексируются поисковыми системами по всему миру. Корректная генерация тегов hreflang и HTTP-заголовка Content-Language гарантирует, что региональные пользователи попадают на соответствующую языковую версию статьи без штрафов за дублирование контента.
i18n/request.ts#В Next.js 16 конфигурация запроса объявляется через фабрику getRequestConfig, которая вызывается один раз на каждый входящий HTTP-запрос:
import { getRequestConfig } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
// Асинхронное получение локали из контекста входящего запроса (Next.js 16)
let locale = await requestLocale;
// Валидация: если локаль отсутствует или не поддерживается, прерываем выполнение
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
// Динамический импорт строго требуемого словаря
messages: (await import(`../messages/${locale}.json`)).default,
// Глобальные форматы дат, времени и валют
formats: {
dateTime: {
short: {
day: 'numeric',
month: 'short',
year: 'numeric',
},
fullWithTime: {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
},
},
number: {
currencyEUR: {
style: 'currency',
currency: 'EUR',
},
currencyUSD: {
style: 'currency',
currency: 'USD',
},
percentage: {
style: 'percent',
minimumFractionDigits: 1,
},
},
},
};
});i18n/routing.ts#import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
export const routing = defineRouting({
// Список всех поддерживаемых языковых локалей
locales: ['en', 'de', 'es', 'ru', 'ja'] as const,
// Локаль по умолчанию при обращении к корневому URL
defaultLocale: 'en',
// Стратегия префикса в URL: 'always' (/en/about) или 'as-needed' (about для default)
localePrefix: 'always',
// Определение локали по заголовкам браузера при первом визите
localeDetection: true,
});
export type Locale = (typeof routing.locales)[number];
// Генерация типобезопасных оберток над навигационными компонентами Next.js
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing);middleware.ts#import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
// Исключаем системные файлы, картинки и API-роуты
matcher: ['/', '/(en|de|es|ru|ja)/:path*', '/((?!_next|_vercel|.*\\..*).*)'],
};Middleware сопоставляет путь /de/products с белым списком локалей. При совпадении формируются заголовки x-next-intl-locale.
Если префикс отсутствует, Middleware автоматически вычисляет оптимальный язык на основе Accept-Language.
app/[locale]/layout.tsx вызывает setRequestLocale(locale) и передает Promise параметров в дерево компонентов.
Необходимо для поддержки генерации статических страниц через generateStaticParams().
Серверные компоненты вызывают getTranslations() и интерполируют строки без гидратационных накладных расходов.
Плюрализация и форматирование дат выполняются нативным C++ движком Intl в рантайме Node.js.
Интерактивные клиентские острова получают минимальный срез словаря через NextIntlClientProvider.
Клиентский бандл защищен от утечки полного словаря приложения.
app/[locale]/layout.tsx#Next.js 16 требует, чтобы params обрабатывались как Promise. Для статической генерации всех языковых версий страниц на этапе сборки экспортируется функция generateStaticParams:
import { notFound } from 'next/navigation';
import { setRequestLocale } from 'next-intl/server';
import { routing, Locale } from '@/i18n/routing';
import { Metadata } from 'next';
interface RootLocaleLayoutProps {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export async function generateMetadata(props: RootLocaleLayoutProps): Promise<Metadata> {
const { locale } = await props.params;
return {
title: {
template: '%s | Global Enterprise SaaS',
default: 'Global Enterprise SaaS',
},
// Формирование hreflang ссылок для SEO
alternates: {
canonical: `https://acme.org/${locale}`,
languages: {
en: 'https://acme.org/en',
de: 'https://acme.org/de',
es: 'https://acme.org/es',
ru: 'https://acme.org/ru',
ja: 'https://acme.org/ja',
'x-default': 'https://acme.org/en',
},
},
};
}
export default async function RootLocaleLayout(props: RootLocaleLayoutProps) {
const { locale } = await props.params;
// Валидация переданного сегмента локали
if (!routing.locales.includes(locale as Locale)) {
notFound();
}
// Фиксация локали для статического рендеринга (SSG / Static Rendering)
setRequestLocale(locale);
return (
<html lang={locale} dir={locale === 'ar' || locale === 'he' ? 'rtl' : 'ltr'}>
<body className="antialiased bg-bg-base text-text-primary">
{props.children}
</body>
</html>
);
}page.tsx#import { getTranslations, setRequestLocale } from 'next-intl/server';
import { getFormatter } from 'next-intl/server';
import { InteractivePaymentButton } from '@/components/checkout/interactive-payment-button';
interface CheckoutPageProps {
params: Promise<{ locale: string }>;
}
export default async function CheckoutPage(props: CheckoutPageProps) {
const { locale } = await props.params;
// Включаем поддержку статической генерации для текущего маршрута
setRequestLocale(locale);
// Извлекаем строго изолированное пространство имен перевода 'Checkout'
const t = await getTranslations('Checkout');
const format = await getFormatter();
const totalAmount = 1499.50;
const itemsCount = 3;
const deliveryDate = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);
return (
<main className="max-w-4xl mx-auto p-8 space-y-6">
<h1 className="text-3xl font-extrabold">{t('pageTitle')}</h1>
{/* Плюрализация по правилам ICU */}
<p className="text-text-muted">
{t('summaryItemsCount', { count: itemsCount })}
</p>
{/* Форматирование валюты с учетом локали */}
<div className="p-4 rounded-xl border border-border bg-bg-surface flex justify-between items-center">
<span className="font-medium text-lg">{t('totalToPay')}:</span>
<span className="font-mono text-2xl font-bold text-accent-cyan">
{format.number(totalAmount, { style: 'currency', currency: locale === 'en' ? 'USD' : 'EUR' })}
</span>
</div>
{/* Форматирование даты */}
<p className="text-sm text-text-subtle">
{t('estimatedDelivery', {
date: format.dateTime(deliveryDate, { dateStyle: 'long' }),
})}
</p>
{/* Интерактивный клиентский компонент: передаем только нужные ключи */}
<InteractivePaymentButton
buttonLabel={t('payButton')}
processingLabel={t('processing')}
amount={totalAmount}
/>
</main>
);
}{
"Checkout": {
"pageTitle": "Оформление заказа",
"summaryItemsCount": "{count, plural, one {# товар} few {# товара} many {# товаров} other {# товара}} в корзине",
"totalToPay": "Итого к оплате",
"estimatedDelivery": "Ориентировочная дата доставки: {date}",
"payButton": "Оплатить заказ",
"processing": "Обработка платежа..."
}
}Команда запустила команду 'next build'. Сборка завершилась ошибкой: 'Error: Next.js could not statically analyze getTranslations in /de/products. Page opted out of static generation'. Что послужило причиной деградации в динамический рендеринг?
Разработчик импортирует корневой словарь import messages from '@/messages/en.json' и передает весь объект в <NextIntlClientProvider messages={messages}> внутри корневого клиентского макета. Это приводит к отправке всех текстов платформы (включая админку и скрытые разделы) в браузер каждого пользователя, раздувая страницу на сотни килобайт.
Как избежать: Используйте метод pick(messages, ['Cart', 'FormValidation']) из lodash или вспомогательную функцию для передачи только тех пространств имен, которые реально используются в клиентских компонентах текущего экрана.
Попытка деструктурировать локаль синхронно: export default function Page({ params }: { params: { locale: string } }) вызовет ошибку во время выполнения:
Error: Route "[locale]" used params.locale. params should be awaited before using its properties.
Как избежать: Всегда типизируйте параметры как params: Promise<{ locale: string }> и ожидайте их разрешения: const { locale } = await props.params;.
Если в конфигурации alternates.languages не указан специальный псевдо-язык 'x-default', международные поисковые роботы (Googlebot, Bingbot) не смогут определить страницу для пользователей из неподдерживаемых регионов, что приведет к падению позиций в поисковой выдаче.
Как избежать: Всегда объявляйте 'x-default' в generateMetadata, указывая международную дефолтную версию (как правило, /en).
Код вида count + " " + (count === 1 ? "товар" : "товаров") корректно работает для двух форм в английском языке (1 item / 2 items), но полностью ломает грамматику в славянских (1 товар, 2 товара, 5 товаров) и азиатских языках.
Как избежать: Всегда используйте синтаксис ICU MessageFormat {count, plural, one {...} few {...} many {...} other {...}}.