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