next-intl
Коротко: Библиотека для многоязычной поддержки (i18n) в проектах Next.js — переводит жёстко заданные тексты интерфейса через центральные файлы переводов.
Подробнее: Тексты хранятся в файлах JSON по языкам (например, messages/de.json, messages/en.json) и в коде извлекаются по ключу через useTranslations/getTranslations (t("key")), а не задаются жёстко. Важно: next-intl переводит только эти тексты интерфейса, но не содержимое базы данных (например, самостоятельно написанные статьи блога) — для этого нужна отдельная функция.
Наш контекст: В Emzett введён для немецкого/английского без префикса в URL (/de/, /en/) — язык вместо этого хранится в cookie. Переход выполнялся постепенно, страница за страницей (подвал, шапка, магазин, тикеты, …), а не сразу.
Подробности
Структура файлов переводов
Файлы переводов — вложенные объекты JSON, чтобы связанные тексты оставались сгруппированными:
{
"Cart": {
"title": "Cart",
"empty": "Your cart is empty.",
"itemCount": "{count, plural, one {# item} other {# items}}"
}
}У такой группировки (одно пространство имён на область интерфейса, например Cart, Checkout, Footer) есть практическая польза помимо простой аккуратности: useTranslations("Cart") возвращает функцию перевода, ограниченную этим пространством имён, так что в остальном коде хватает более коротких ключей (t("title") вместо t("Cart.title")), а конфликты имён между областями исключены.
Синтаксис сообщений ICU
itemCount показывает важную возможность next-intl: синтаксис сообщений ICU для форм множественного числа — вместо того чтобы самим писать в коде логику count === 1 ? "item" : "items" (которая быстро оказывается неверной для языков с более сложными правилами множественного числа, чем немецкий/английский), библиотека делает это автоматически и правильно для каждого языка. В некоторых языках (например, польском, русском) больше двух форм множественного числа (одна для «один», одна для «несколько», одна для «много») — синтаксис ICU отображает это через дополнительные категории (few, many), не требуя в коде приложения языковых различий. Условные тексты тоже можно выразить таким образом, например грамматический род через синтаксис select ({gender, select, male {he} female {she} other {it}}).
Использование на стороне сервера и клиента
В серверных компонентах используется getTranslations() (асинхронный), в клиентских — useTranslations() (хук, синхронный, поскольку переводы уже доставлены сервером). Оба обращаются к тем же файлам JSON, но механизм за ними различается: на стороне сервера сообщения читаются прямо из файловой системы; на стороне клиента они предоставляются через NextIntlClientProvider (передаёт нужные переводы клиенту как props) — чтобы пакет JavaScript, отправляемый в браузер, оставался небольшим, нужно передавать только те сообщения, которые действительно нужны клиентским компонентам.
Маршрутизация на cookie и на URL
Альтернативой подходу на cookie (который использует Emzett) была бы маршрутизация по URL (/de/shop, /en/shop) — преимущество: поисковые системы могут индексировать каждую языковую версию отдельно, а общие URL однозначно привязаны к языку (важно для международного SEO через теги hreflang); недостаток: больше сложности маршрутизации (каждый маршрут фактически существует дважды), а пользователи, переходящие по неверно общим ссылкам, могут оказаться на неверном языке вместо автоматического попадания на предпочитаемый. Подход на cookie проще в реализации и сохраняет «чистые» URL без языкового префикса, но имеет недостаток: один и тот же URL может показывать разное содержимое в зависимости от cookie — это неоптимально с чисто SEO-точки зрения, но обычно не является практической проблемой для проекта, ориентированного в первую очередь на один языковой регион.
См. также: Next.js App Router, i18n