Мультямовність у сучасних веб-додатках: Інтеграція next-intl, SEO-френдлі слагом та правильна архітектура
Вихід на міжнародний ринок вимагає від веб-додатка не лише якісного перекладу, а й правильної технічної бази. Пошукові системи повинні чітко розуміти, яку мовну версію сторінки індексувати, а користувачі — отримувати миттєву відповідь рідною мовою зі зручним URL (наприклад, /en/products або /uk/products).
Сьогодні ми розберемо, як побудувати таку систему з нуля. Наш інструментарій — Next.js 16 та бібліотека next-intl. Це стандарт індустрії, який ідеально працює з App Router, підтримує Server Components та забезпечує сувору типізацію ключів перекладу.
Крок 1: Встановлення бази
Для початку додамо next-intl у наш проєкт. Використовуємо швидкий та надійний пакетний менеджер:
pnpm add next-intl
Цього достатньо, щоб почати будувати фундамент. Але перш ніж писати конфіги, нам потрібно навести ідеальний порядок у файловій структурі.
Крок 2: Архітектура Route Groups — розділяй і володарюй
Найбільша помилка новачків — звалювати всі сторінки в один кореневий каталог src/app, намагаючись обгорнути весь додаток одним гігантським провайдером. Ми підемо шляхом професіоналів і використаємо Route Groups (папки в дужках), які дозволяють логічно розділити додаток без впливу на URL.
Наша структура src/app виглядатиме так:
src/
└── app/
├── (admin)/
│ ├── layout.tsx
│ └── page.tsx
├── (locale)/
│ └── [locale]/
│ ├── layout.tsx
│ └── page.tsx
├── (root)/
│ ├── layout.tsx
│ └── page.tsx
└── proxy.ts
Чому саме такий розподіл і що має бути всередині?
Важливе правило: Кожна з цих груп є повністю ізольованим середовищем. Це означає, що кожному каталогу потрібен свій власний layout.tsx (де ти визначаєш <html>, <body> і специфічні обгортки) та базовий page.tsx.
1. Каталог (locale): Клієнтська частина
Це серце нашого мультямовного сайту. Всередині ми створюємо динамічний сегмент [locale]. Усі сторінки, які потребують SEO-індексації та мовного префіксу в URL (наприклад, головна сторінка, каталог товарів, блог), житимуть тут.
Layout: Тут ми ініціалізуємо словники
next-intl, підключаємо глобальні стилі для клієнтської частини, хедер з перемикачем мов та футер.
2. Каталог (admin): Панель керування
Адмінка — це зовсім інший світ. Їй часто не потрібен мовний префікс в URL (адмін може працювати за адресою /admin), тут інший дизайн, інші провайдери (наприклад, специфічні для дашборду) і жодних вимог до SEO.
Layout: Містить сайдбар адміністратора, провайдери авторизації та перевірку прав доступу, але не перевантажується клієнтськими словниками всього сайту.
3. Каталог (root): Технічна зона
Сюди потрапляють роути, які повинні працювати без мовного префіксу. Це можуть бути сторінки помилок, системні вебхуки або API-ендпоінти.
Layout: Максимально легкий і базовий, лише для того, щоб Next.js міг відрендерити сторінку без помилок нестачі кореневих тегів.
Крок 3: Маршрутизація нового покоління
Раніше для перехоплення запитів і визначення мови користувача ми б використовували middleware.ts. Проте в архітектурі Next.js 16 підхід змінився: тепер за цей функціонал (як і за Auth) відповідає proxy.ts, який працює швидше і краще інтегрується з новітніми серверними можливостями фреймворку. Він буде перехоплювати вхідний запит, аналізувати заголовки браузера (Accept-Language) і перенаправляти користувача на правильний [locale] роут.
Крок 4: Розбираємо код на атоми
Давай подивимось, як налаштувати кожен із наших ізольованих каталогів, щоб Next.js, next-intl та пошукові роботи працювали як єдиний механізм.
1. Технічна зона: src/app/(root)
Що відбувається, коли користувач просто вводить твойдoмен.com без вказівки мови? Щоб уникнути дублювання контенту (найбільший ворог SEO), ми маємо миттєво перенаправити його на дефолтну мовну версію (наприклад, /uk).
src/app/(root)/layout.tsx Тут ми створюємо максимально базовий каркас, щоб Next.js міг коректно обробити сторінку перед редиректом.
TypeScript
export default function RootRedirectLayout({ children }: { children: React.ReactNode }) {
// 🟢 ВИПРАВЛЕННЯ: Додали атрибут lang="uk" (компілятор більше не сваритиметься)
return (
<html lang="uk">
<body>{children}</body>
</html>
)
}
src/app/(root)/page.tsx А ось тут відбувається магія маршрутизації на сервері:
TypeScript
import { redirect } from 'next/navigation'
import { defaultLocale } from '@/i18n/routing'
export default function RootPage() {
// Якщо користувач зайшов на "/", редиректим на "/uk"
redirect(`/${defaultLocale}`)
}
2. Зона Адміністратора: src/app/(admin)
Для адмінки нам не потрібна SEO-оптимізація чи префікси в URL (ми не хочемо URL типу /uk/admin/dashboard). Але ми все одно хочемо мати можливість використовувати функцію t() для перекладу інтерфейсу. Тому ми "жорстко" прив'язуємо адмінку до дефолтної мови.
src/app/(admin)/layout.tsx
TypeScript
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
import type React from 'react'
import { Toaster } from 'react-hot-toast'
import { routing } from '@/i18n/routing'
// 1. ВСЕ СТИЛІ АДМІНКИ І ЛОГІНА
import '@/assets-admin/css/animate.min.css'
import '@/assets-admin/css/animation.css'
import '@/assets-admin/css/bootstrap.css'
import '@/assets-admin/css/bootstrap-select.min.css'
import '@/assets-admin/css/styles.css'
import '@/assets-admin/font/fonts.css'
import '@/assets-admin/icon/style.css'
export default async function AdminGroupLayout({ children }: { children: React.ReactNode }) {
// Оскільки адмінка не мультимовна, примусово беремо дефолтну локаль
const locale = routing.defaultLocale
// Отримуємо повідомлення для цієї локалі, щоб t() працював у адмінці
const messages = await getMessages({ locale })
return (
<html lang={locale}>
<body>
{/* ✅ Обов'язково обертаємо у провайдер, інакше клієнтські компоненти впадуть */}
<NextIntlClientProvider locale={locale} messages={messages}>
{/* 🟢 ДОДАНО КОМПОНЕНТ ПОВІДОМЛЕНЬ */}
<Toaster
position="top-right"
reverseOrder={false}
toastOptions={{
duration: 4000,
style: {
background: '#333',
color: '#fff',
zIndex: 99999,
},
}}
/>
{children}
</NextIntlClientProvider>
</body>
</html>
)
}
Що тут головне? Ми примусово отримуємо словник через getMessages({ locale }) і передаємо його в <NextIntlClientProvider>. Без цього клієнтські компоненти адмінки впадуть з помилкою, оскільки вони не знайдуть контексту перекладів.
3. Серце додатку: src/app/[locale]/layout.tsx
Це найважливіший файл. Саме він відповідає за те, щоб Google правильно індексував твої українські та англійські сторінки, а користувач отримував миттєвий відгук завдяки SSG (Static Site Generation).
Розберемо ключові налаштування:
TypeScript
import type { Metadata } from 'next'
import { Rajdhani, Rubik } from 'next/font/google'
import Script from 'next/script'
import { NextIntlClientProvider } from 'next-intl'
import { getMessages, setRequestLocale } from 'next-intl/server'
import type React from 'react'
import { Toaster } from 'react-hot-toast'
import LayoutWrapper from '@/components/common/LayoutWrapper'
import type { Locale } from '@/i18n/routing'
import { routing } from '@/i18n/routing'
import { ThemeProvider } from '@/providers/ThemeProvider'
export const revalidate = 60
import '@/assets-main/css/tailwind.css'
import '@/assets-main/scss/main.scss'
import '@/assets-main/css/odometer-theme-default.css'
// Налаштування шрифтів
const rajdhani = Rajdhani({ subsets: ['latin'], weight: ['300', '400', '500', '600', '700'] })
const rubik = Rubik({
subsets: ['latin'],
weight: ['300', '400', '500', '600', '700', '800', '900'],
})
interface LocaleLayoutProps {
children: React.ReactNode
params: Promise<{ locale: string }>
}
// 🟢 Генеруємо параметри для SSG
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }))
}
// 1. Базовий URL та ID GTM
const baseUrl = 'https://muntai.top'
const GTM_ID = 'GTM-ваш код'
const seoData = {
uk: {
suffix: 'Lead Fullstack Розробник | Next.js, TypeScript, Prisma',
description:
'Створюю швидкі, масштабовані та безпечні веб-платформи. Спеціалізуюся на сучасній Fullstack-розробці (Next.js, TypeScript, PostgreSQL). Повний цикл: від архітектури бази даних до налаштування Linux VPS та деплою.',
siteName: 'Muntai - Сучасна веб-розробка | Next.js Експерт',
},
en: {
suffix: 'Lead Fullstack Developer | Next.js, TypeScript, Prisma',
description:
'Building fast, scalable, and secure web applications. Specializing in modern Fullstack development (Next.js, TypeScript, PostgreSQL). End-to-end solutions from database architecture to Linux VPS deployment.',
siteName: 'Muntai - Modern Web Development | Next.js Expert',
},
}
export async function generateMetadata({ params }: LocaleLayoutProps): Promise<Metadata> {
const { locale } = await params
// Сувора типізація ключа
const currentSeo = seoData[locale as keyof typeof seoData] || seoData.en
return {
metadataBase: new URL(baseUrl),
title: `Muntai - ${currentSeo.suffix}`,
description: currentSeo.description,
alternates: {
canonical: `${baseUrl}/${locale}`,
languages: {
uk: `${baseUrl}/uk`,
en: `${baseUrl}/en`,
},
},
openGraph: {
title: `Muntai - ${currentSeo.suffix}`,
description: currentSeo.description,
url: `${baseUrl}/${locale}`,
siteName: currentSeo.siteName,
type: 'website',
locale: locale,
images: [
{
url: '/assets-main/images/logo/muntai-image.jpg',
width: 1200,
height: 896,
alt: 'Muntai - Next.js & TypeScript Expert',
},
],
},
}
}
export default async function LocaleLayout(props: LocaleLayoutProps) {
const params = await props.params
const locale = params.locale as Locale
// Повідомляємо серверним компонентам про поточну локаль
setRequestLocale(locale)
// Отримуємо переклади
const messages = await getMessages({ locale })
const recaptchaKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY || ''
return (
<html lang={locale} suppressHydrationWarning>
<body className={`${rajdhani.className} ${rubik.className}`} suppressHydrationWarning>
<NextIntlClientProvider locale={locale} messages={messages}>
{/* 2. Google Tag Manager (noscript) */}
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
height="0"
width="0"
style={{ display: 'none', visibility: 'hidden' }}
title="Google Tag Manager"
/>
</noscript>
{/* 1. Google Tag Manager (Script) */}
<Script id="gtm-script" strategy="afterInteractive">
{`
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${GTM_ID}');
`}
</Script>
<Script src="/assets-main/js/smooth.js" strategy="lazyOnload" />
{/* Pyodide Engine для "The Lab" */}
<Script
id="pyodide-engine"
src="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.js"
strategy="lazyOnload"
/>
{/* reCAPTCHA v3 */}
{recaptchaKey && (
<Script
id="recaptcha-script"
src={`https://www.google.com/recaptcha/api.js?render=${recaptchaKey}`}
strategy="afterInteractive"
/>
)}
<ThemeProvider>
<LayoutWrapper>
<Toaster position="top-right" />
{props.children}
</LayoutWrapper>
</ThemeProvider>
</NextIntlClientProvider>
</body>
</html>
)
}
Чому ця конфігурація ідеальна? Три стовпи архітектури
1. SEO та Hreflang (Блок generateMetadata) Ми динамічно формуємо Title та Description залежно від того, що прийшло в параметрі locale. Але найголовніше тут — це об'єкт alternates. Ми явно кажемо Google: "Дивись, ця сторінка доступна двома мовами". Завдяки languages: { uk: ..., en: ... } пошуковик розуміє зв'язок між сторінками і показує у видачі правильну версію для кожного регіону.
2. SSG: Швидкість має значення (generateStaticParams) Функція generateStaticParams() повертає масив можливих локалей (наприклад, ['uk', 'en']). Це сигнал для Next.js під час білду (на етапі збірки в Docker) заздалегідь згенерувати статичні HTML-версії сторінок для кожної мови. Результат? Миттєве завантаження без очікування рендеру на сервері.
3. Зв'язок Сервера та Клієнта (setRequestLocale та NextIntlClientProvider) Оскільки ми використовуємо Server Components (App Router), next-intl потребує знати поточну локаль на рівні сервера.
setRequestLocale(locale)— фіксує мову для серверних компонентів.getMessages({ locale })— підтягує словник із JSON-файлу.<NextIntlClientProvider messages="{messages}">— це міст, який передає словники в клієнтські компоненти ("use client"), дозволяючи їм миттєво перемикати текст без додаткових запитів до бекенду.
Ця структура гарантує, що твій додаток буде строго типізованим, швидко індексуватиметься Google і легко масштабуватиметься, коли ти вирішиш додати ще 5 нових мов.
Крок 5: Мозок мультямовності — каталог src/i18n/
Наші layout.tsx готові приймати локаль, але звідки Next.js знає, які мови взагалі існують і як з ними працювати? Для цього ми створюємо ізольований каталог src/i18n/, який слугуватиме єдиним джерелом правди для всього додатка.
Тут ми розмістимо три файли. Давай розберемо кожен з них.
1. Базові правила: src/i18n/routing.ts
Цей файл — фундамент. Він каже системі, які мови ми підтримуємо і як вони мають відображатися в URL.
TypeScript
import { defineRouting } from 'next-intl/routing'
export const routing = defineRouting({
locales: ['uk', 'en'],
defaultLocale: 'uk',
localePrefix: 'always', // URL завжди матиме префікс (напр., /uk/about)
})
export const defaultLocale = routing.defaultLocale
// Експортуємо тип Locale для використання у layout.tsx
export type Locale = (typeof routing.locales)[number]
Чому це професійно: Зверни увагу на останній рядок. Ми не просто хардкодимо рядки по всьому проєкту, ми створюємо суворий тип Locale. Тепер, якщо хтось у команді спробує передати в компонент мову 'fr' замість 'uk' або 'en', TypeScript видасть помилку ще до етапу компіляції. Це рятує від сотень неочевидних багів.
2. Розумна навігація: src/i18n/navigation.ts
У мультямовному додатку ти не можеш використовувати стандартні Link та useRouter з пакета next/navigation. Чому? Тому що стандартний лінк <Link href="/about"> перекине користувача на корінь, загубивши його поточну мову.
Нам потрібні "розумні" обгортки, які знають про контекст:
TypeScript
import { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'
// Легкові обгортки для навігаційних API Next.js, які враховують конфігурацію маршрутизації
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing)
Що це дає: Тепер ти імпортуєш <Link> саме з цього файлу. Якщо англомовний користувач перебуває на сторінці /en/products і натискає на <Link href="/about">, ця обгортка автоматично підставить префікс, і клієнт перейде на /en/about. Навігація стає безшовною.
3. Серверний обробник та Захист від падінь: src/i18n/request.ts
Цей файл викликається "під капотом" при кожному серверному запиті. Його задача — взяти локаль, підтягнути правильний JSON-файл зі словником і віддати його компонентам. Але тут є кілька геніальних інженерних рішень:
TypeScript
import { getRequestConfig } from 'next-intl/server'
import { routing } from './routing'
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
// Тільки валідація
if (!locale || !(routing.locales as readonly string[]).includes(locale)) {
locale = routing.defaultLocale
}
// Завантажуємо повідомлення без внутрішніх try/catch з підміною локалі
const messages = (await import(`../messages/${locale}.json`)).default
return {
locale,
messages,
timeZone: 'Europe/Kyiv',
// 🛡 ЗАХИСТ СЕРВЕРА ВІД ПАДІННЯ
onError(error) {
if (error.code === 'INVALID_MESSAGE') {
console.error(
`[next-intl] Синтаксична помилка перекладу в локалі "${locale}":`,
error.message,
)
} else {
console.error(error)
}
},
getMessageFallback({ key, namespace }) {
return namespace ? `${namespace}.${key}` : key
},
}
})
Круті фішки цього файлу:
Динамічний імпорт JSON:
await import(...)гарантує, що ми не тягнемо в пам'ять сервера відразу всі мови світу. Завантажується лише той словник, який потрібен користувачу в дану мілісекунду.Локалізація часу:
timeZone: 'Europe/Kyiv'задає єдиний стандарт для форматування дат. Це критично важливо, якщо твій сервер фізично знаходиться, наприклад, у Німеччині, але бізнес працює за українським часом.Броня від крашів (
onErrorтаgetMessageFallback): Що станеться, якщо розробник додав новий компонент, але забув прописати ключ перекладу вuk.json? Зазвичай сервер падає з 500-ю помилкою. Наш код перехоплює це! Замість падіння, користувач просто побачить сирий ключ (наприклад,Header.about_us), а в консоль сервера впаде акуратний лог. Додаток продовжує працювати.
Крок 6: Головний диспетчер — proxy.ts (Next-Auth v5 + next-intl)
У нових версіях Next.js 16 (особливо при використанні Auth v5) архітектура змінилася. Замість класичного middleware.ts ми використовуємо proxy.ts, який бере на себе роль головного "шлюзу" (gatekeeper) для всіх вхідних запитів.
Саме тут ми маємо подружити дві складні системи: авторизацію (Next-Auth) та мультямовність (next-intl). Завдання ускладнюється тим, що публічна частина сайту повинна мати префікси в URL (/uk/about), а адмінка — ні (/admin), але при цьому адмінка теж повинна мати доступ до перекладів.
Ось як виглядає елегантне вирішення цієї задачі:
TypeScript
import { type NextRequest, NextResponse } from 'next/server'
import type { Session } from 'next-auth'
import createIntlMiddleware from 'next-intl/middleware'
import { auth } from '@/auth'
import { routing } from './i18n/routing'
const { locales, defaultLocale } = routing
// Ініціалізуємо стандартний мідлвар інтернаціоналізації
const handleI18n = createIntlMiddleware({
locales,
defaultLocale,
localeDetection: false, // Ми примусово використовуємо defaultLocale, якщо префіксу немає
})
// Обгортаємо весь проксі у функцію auth з Auth v5
export default auth((req: NextRequest & { auth: Session | null }) => {
const { nextUrl } = req
const isLoggedIn = !!req.auth
const pathname = nextUrl.pathname
// Визначаємо зони (роути)
const isAdmin = pathname === '/admin' || pathname.startsWith('/admin/')
const isLogin = pathname === '/login' || pathname.startsWith('/login/')
// --- 1. Обробка LOGIN ---
if (isLogin) {
if (isLoggedIn) {
// Якщо вже авторизований - кидаємо в адмінку
return NextResponse.redirect(new URL('/admin', nextUrl))
}
// Для логіна примусово ставимо локаль за замовчуванням
const response = NextResponse.next()
response.headers.set('x-next-intl-locale', defaultLocale)
return response
}
// --- 2. Обробка ADMIN ---
if (isAdmin) {
if (!isLoggedIn) {
// Якщо не авторизований - кидаємо на логін і запам'ятовуємо куди він хотів зайти
const url = new URL('/login', nextUrl)
url.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(url)
}
/**
* ВАЖЛИВИЙ МОМЕНТ: Адмінка працює без префікса мови в URL.
* Але щоб функція t() працювала в адмінці без помилок,
* сервер повинен знати поточну локаль. Ми передаємо її через заголовок.
*/
const response = NextResponse.next()
response.headers.set('x-next-intl-locale', defaultLocale)
return response
}
// --- 3. Для всього іншого (Публічний сайт) ---
// Тут handleI18n сам розбереться з префіксами /uk, /en та редиректами
return handleI18n(req)
})
export const config = {
// Виключаємо статику та API, щоб не ганяти проксі вхолосту і не навантажувати сервер
matcher: ['/((?!api|_next|_vercel|assets-admin|assets-main|.*\\..*).*)'],
}
Розбираємо магію proxy.ts: Чому це працює ідеально?
1. Обгортка auth (Auth v5) У 5-й версії Next-Auth ми імпортуємо auth із нашого файлу конфігурації auth.ts і повністю обгортаємо ним наш проксі-обробник. Це дає нам миттєвий доступ до об'єкта req.auth. Ми знаємо, чи залогінений користувач ще до того, як відрендериться хоча б один піксель сторінки.
2. Вирішення конфлікту "Безпрефіксних" зон Найбільший біль при налаштуванні next-intl — це сторінки, які виключені з мультямовності (як наш /admin або /login). next-intl вимагає знати локаль, інакше він "викидає" помилку. Ми вирішили це максимально витончено: ми пропускаємо запит далі (NextResponse.next()), але непомітно "підкладаємо" в HTTP-заголовки рядок x-next-intl-locale: uk. Тепер адмінка спокійно завантажується без /uk/ в URL, а словники працюють як годинник.
3. Економія ресурсів сервера (Matcher) Початківці часто забувають про масив matcher. Якщо його не налаштувати правильно, твій proxy.ts буде запускатися для кожної картинки, кожного CSS-файлу та кожного фавікону. У нашому конфігу прописано регулярний вираз, який відсікає всі запити до папок assets-main, assets-admin, _next та api. Проксі відпрацьовує тільки тоді, коли користувач реально переходить між сторінками. Це колосально економить ресурси твого VPS.
Підсумок
Налаштування мультямовності в Next.js 16 — це не просто встановлення одного пакета. Це грамотна архітектура, де кожен елемент виконує свою роль:
Route Groups (
(root),(locale),(admin)) розділяють логіку та ізолюють провайдери.next.config.ts(Standalone) таproxy.tsзабезпечують блискавичну роботу на сервері.Сувора типізація TypeScript у файлах конфігурації унеможливлює помилки з ключами.
Використовуючи цей підхід, ти отримуєш гнучку, SEO-оптимізовану систему, яка готова до масштабування хоч на 20 мов світу, зберігаючи при цьому найвищу продуктивність та ідеальну оцінку в Google PageSpeed.
Крок 7: Словники перекладів (src/messages/)
Уся наша архітектура будується навколо JSON-файлів, які зберігають тексти. Ми створюємо папку src/messages/ і додаємо туди наші словники. Головне правило тут — структура. Не скидай всі переклади в один рівень, використовуй вкладені об'єкти (namespaces), щоб розбити текст по сторінках або компонентах.
src/messages/uk.json
JSON
{
"HomePage": {
"title": "Сучасна веб-розробка",
"subtitle": "Створюємо швидкі та безпечні платформи",
"cta_button": "Обговорити проєкт"
},
"Navigation": {
"about": "Про нас",
"services": "Послуги",
"contact": "Контакти"
}
}
src/messages/en.json
JSON
{
"HomePage": {
"title": "Modern Web Development",
"subtitle": "Building fast and secure platforms",
"cta_button": "Discuss a project"
},
"Navigation": {
"about": "About",
"services": "Services",
"contact": "Contact"
}
}
Лайфхак: Оскільки ми налаштували строгу типізацію, якщо ти спробуєш викликати ключ HomePage.title в коді, а він випадково видалений з en.json, TypeScript одразу підкреслить це червоним. Ти ніколи не відправиш у продакшен сторінку з "дірками" в тексті.
Крок 8: Як це виглядає на практиці (Приклад сторінки)
Тепер давай подивимось, як легко використовувати ці переклади в нашому суворо типізованому коді Next.js 16. Ми використовуємо Server Components, тому переклад відбувається прямо на сервері, миттєво віддаючи браузеру готовий HTML.
Ось наша головна сторінка: src/app/[locale]/page.tsx
TypeScript
import { getTranslations } from 'next-intl/server'
import { Link } from '@/i18n/navigation'
export default async function HomePage() {
// Викликаємо словник саме для простору імен "HomePage"
// Це серверний компонент, тому використовуємо getTranslations з '/server'
const t = await getTranslations('HomePage')
const nav = await getTranslations('Navigation')
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
{/* Навігація з нашою розумною обгорткою Link */}
<nav className="mb-12 flex gap-6">
<Link href="/about" className="text-lg hover:text-blue-500">
{nav('about')}
</Link>
<Link href="/services" className="text-lg hover:text-blue-500">
{nav('services')}
</Link>
</nav>
{/* Основний контент сторінки */}
<div className="text-center">
<h1 className="mb-4 text-6xl font-bold tracking-tight">
{t('title')}
</h1>
<p className="mb-8 text-xl text-gray-600">
{t('subtitle')}
</p>
<button className="rounded-lg bg-blue-600 px-8 py-3 text-white transition hover:bg-blue-700">
{t('cta_button')}
</button>
</div>
</main>
)
}
Зверни увагу на ключові відмінності:
Серверний рендер: Ми використовуємо
async/awaitтаgetTranslations('HomePage'). Це означає, що клієнту не відправляються зайві JavaScript-бібліотеки для перекладу. Він отримує чистий HTML з уже підставленим текстом ("Сучасна веб-розробка").Розумний
Link: Зверни увагу на імпортimport { Link } from '@/i18n/navigation'. Коли користувач натисне на посилання/about, ця обгортка автоматично зрозуміє його поточну мову і перенаправить на/uk/aboutабо/en/about.
Підсумок
Створення мультямовності — це не просто заміна одного тексту на інший. Це побудова надійної інфраструктури. Використовуючи next-intl у зв'язці з App Router, proxy.ts та TypeScript, ти отримуєш систему рівня Enterprise.
Твій сайт літає завдяки SSG, Google ідеально індексує всі мовні версії, адмінка живе своїм ізольованим життям, а ти, як розробник, кайфуєш від відсутності багів завдяки строгій типізації словників.

Коментарі (0)
Залишити коментар