Пошук уроків, статей та іншого контенту
Організуйте локалізацію застосунку, визначення мови та fallback для перекладів.
Інтернаціоналізація — це підготовка застосунку до роботи з кількома мовами. Вона охоплює:
зберігання перекладів окремо від компонентів;
визначення мови користувача;
вибір мови за URL;
fallback, якщо для поточної мови відсутній переклад;
передавання локалі в HTML та серверні компоненти.
У Next.js з App Router зручно використовувати сегмент маршруту:
/uk/about
/en/aboutУ цьому випадку uk або en є частиною URL і передається в сегмент [lang].
Приклад структури файлів:
app/
[lang]/
layout.tsx
page.tsx
dictionaries/
en.ts
uk.ts
lib/
i18n.ts
middleware.tsВикористаємо дві локалі:
uk — українська;
en — англійська.
Українська буде локаллю за замовчуванням і fallback-локаллю.
Замість розміщення текстів безпосередньо в JSX створимо окремі словники.
dictionaries/uk.tsexport const dictionary = {
"home.title": "Ласкаво просимо",
"home.description": "Це приклад багатомовного застосунку.",
"navigation.home": "Головна",
"navigation.about": "Про нас",
"button.readMore": "Докладніше",
} as const;dictionaries/en.tsexport const dictionary = {
"home.title": "Welcome",
"home.description": "This is an example of a multilingual application.",
"navigation.home": "Home",
"navigation.about": "About us",
"button.readMore": "Read more",
} as const;Ключі мають бути стабільними, а переклади можуть змінюватися незалежно від компонентів.
Створимо модуль lib/i18n.ts, який:
описує доступні локалі;
завантажує потрібний словник;
додає переклади з fallback-локалі;
повертає переклад за ключем.
lib/i18n.tsimport { dictionary as en } from "../dictionaries/en";
import { dictionary as uk } from "../dictionaries/uk";
export const locales = ["uk", "en"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "uk";
type Dictionary = Record<string, string>;
const dictionaries: Record<Locale, Dictionary> = {
uk,
en,
};
export function isLocale(value: string): value is Locale {
return locales.includes(value as Locale);
}
export function getDictionary(locale: Locale): Dictionary {
const fallback = dictionaries[defaultLocale];
const current = dictionaries[locale];
// Переклади поточної локалі мають пріоритет над fallback.
return {
...fallback,
...current,
};
}
export function translate(
dictionary: Dictionary,
key: string,
): string {
return dictionary[key] ?? key;
}Оператор розгортання створює fallback:
{
...fallback,
...current,
}Якщо ключ є в поточному словнику, використовується його значення. Якщо ключа немає, залишається значення з українського словника.
Якщо ключ відсутній у всіх словниках, функція повертає сам ключ. Це допомагає помітити помилку під час розробки.
Створимо динамічний сегмент app/[lang].
app/[lang]/layout.tsximport type { ReactNode } from "react";
import { notFound } from "next/navigation";
import {
isLocale,
locales,
type Locale,
} from "../../lib/i18n";
type LayoutProps = {
children: ReactNode;
params: Promise<{
lang: string;
}>;
};
export function generateStaticParams() {
return locales.map((lang) => ({
lang,
}));
}
export default async function Layout({
children,
params,
}: LayoutProps) {
const { lang } = await params;
if (!isLocale(lang)) {
notFound();
}
return (
<html lang={lang}>
<body>{children}</body>
</html>
);
}generateStaticParams повідомляє Next.js, для яких локалей потрібно підготувати маршрути.
Перевірка isLocale не дозволяє обробляти невідомі локалі:
/uk
/enє правильними маршрутами, а:
/de
/frбудуть передані до notFound().
Атрибут lang важливий для:
доступності;
програм читання з екрана;
пошукових систем;
правильного визначення мови сторінки браузером.
app/[lang]/page.tsximport Link from "next/link";
import { notFound } from "next/navigation";
import {
getDictionary,
isLocale,
translate,
} from "../../lib/i18n";
type PageProps = {
params: Promise<{
lang: string;
}>;
};
export default async function HomePage({
params,
}: PageProps) {
const { lang } = await params;
if (!isLocale(lang)) {
notFound();
}
const dictionary = getDictionary(lang);
return (
<main>
<nav>
<Link href={`/${lang}`}>
{translate(dictionary, "navigation.home")}
</Link>
{" · "}
<Link href={`/${lang}/about`}>
{translate(dictionary, "navigation.about")}
</Link>
</nav>
<h1>
{translate(dictionary, "home.title")}
</h1>
<p>
{translate(dictionary, "home.description")}
</p>
<button type="button">
{translate(dictionary, "button.readMore")}
</button>
</main>
);
}Тепер сторінки працюватимуть за адресами:
/uk
/enДля /uk будуть використані українські переклади, а для /en — англійські.
Компонент отримує локаль як параметр маршруту, а не з глобального стану. Це робить мову сторінки передбачуваною та дозволяє безпосередньо відкривати або зберігати локалізовані URL.
Коли користувач відкриває кореневу адресу /, локаль ще невідома. Її можна визначити в такому порядку:
за cookie NEXT_LOCALE;
за заголовком Accept-Language;
використати локаль за замовчуванням.
Заголовок Accept-Language браузер надсилає автоматично. Наприклад:
uk-UA,uk;q=0.9,en;q=0.8Створимо Middleware, який перенаправляє користувача з / на локалізований маршрут.
middleware.tsimport { NextRequest, NextResponse } from "next/server";
import {
defaultLocale,
isLocale,
locales,
type Locale,
} from "./lib/i18n";
function localeFromAcceptLanguage(
header: string | null,
): Locale {
if (!header) {
return defaultLocale;
}
const languages = header
.split(",")
.map((part) => {
const [language, quality = "q=1"] = part.trim().split(";");
return {
language: language.toLowerCase(),
quality: Number(quality.replace("q=", "")),
};
})
.sort((a, b) => b.quality - a.quality);
for (const item of languages) {
const language = item.language.split("-")[0];
if (locales.includes(language as Locale)) {
return language as Locale;
}
}
return defaultLocale;
}
function detectLocale(request: NextRequest): Locale {
const cookieLocale = request.cookies.get("NEXT_LOCALE")?.value;
if (cookieLocale && isLocale(cookieLocale)) {
return cookieLocale;
}
return localeFromAcceptLanguage(
request.headers.get("accept-language"),
);
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const firstSegment = pathname.split("/")[1];
if (isLocale(firstSegment)) {
return NextResponse.next();
}
const locale = detectLocale(request);
const localizedPath =
pathname === "/"
? `/${locale}`
: `/${locale}${pathname}`;
return NextResponse.redirect(
new URL(localizedPath, request.url),
);
}
export const config = {
matcher: [
"/",
"/((?!_next|api|.*\\..*).*)",
],
};Тепер:
/ перенаправляється, наприклад, на /uk;
/about перенаправляється на /uk/about або /en/about;
/uk і /en обробляються без додаткового перенаправлення;
службові файли та маршрути API не проходять через цю логіку.
Розглянемо словники:
// uk.ts
export const dictionary = {
"home.title": "Ласкаво просимо",
"home.description": "Опис українською",
"navigation.home": "Головна",
} as const;// en.ts
export const dictionary = {
"home.title": "Welcome",
"navigation.home": "Home",
} as const;Для локалі en результат getDictionary("en") буде еквівалентний такому об’єкту:
{
"home.title": "Welcome",
"home.description": "Опис українською",
"navigation.home": "Home",
}home.description відсутній в англійському словнику, тому використовується українське значення.
Такий підхід зручний як тимчасовий fallback, але для готового продукту варто перевіряти відсутні переклади автоматично. Інакше користувач може побачити текст іншою мовою.
Для перемикання мови достатньо створити посилання на той самий маршрут з іншою локаллю:
import Link from "next/link";
type LanguageSwitcherProps = {
currentPath: string;
};
export function LanguageSwitcher({
currentPath,
}: LanguageSwitcherProps) {
return (
<nav aria-label="Вибір мови">
<Link href={`/uk${currentPath}`}>
Українська
</Link>
{" | "}
<Link href={`/en${currentPath}`}>
English
</Link>
</nav>
);
}Наприклад, на сторінці /uk/about:
посилання українською веде на /uk/about;
посилання англійською веде на /en/about.
URL залишається джерелом правди для поточної мови. Це краще, ніж зберігати локаль лише у стані клієнтського компонента, оскільки локалізовану сторінку можна скопіювати, оновити або відкрити в новій вкладці.
Погано:
<h1>Ласкаво просимо</h1>Якщо текст має бути локалізованим, він повинен надходити зі словника:
<h1>{translate(dictionary, "home.title")}</h1>Не варто напряму використовувати значення з URL:
const dictionary = getDictionary(lang as Locale);Так можна передати довільний рядок замість підтримуваної локалі. Спочатку потрібно виконати перевірку через isLocale.
Погано:
<Link href="/about">Про нас</Link>Таке посилання втратить поточну мову. Потрібно додавати локаль:
<Link href={`/${lang}/about`}>
{translate(dictionary, "navigation.about")}
</Link>Якщо тексти розподілені по JSX, їх складно:
перевіряти;
передавати перекладачам;
синхронізувати між мовами;
повторно використовувати.
Краще зберігати їх у словниках, а компоненти залишати відповідальними за структуру інтерфейсу.
Якщо код просто звертається до ключа:
dictionary[key]результатом може бути undefined. Fallback дозволяє показати запасний переклад або хоча б сам ключ для діагностики.
langHTML без правильного lang може некоректно оброблятися програмами читання з екрана та пошуковими системами. Локаль потрібно передавати в:
<html lang={lang}>Локаль можна представити динамічним сегментом [lang].
Переклади краще зберігати в окремих словниках.
Middleware визначає мову за cookie або Accept-Language.
Локалізовані URL мають вигляд /uk/page або /en/page.
Fallback дозволяє використовувати переклад з локалі за замовчуванням, якщо поточний переклад відсутній.
Перед використанням локаль потрібно перевіряти.
Посилання між сторінками повинні зберігати поточну локаль.
Атрибут lang у <html> має відповідати локалі сторінки.