Internacjonalizacja, zwana także i18n, to proces dostosowywania aplikacji lub produktu do różnych rynków językowych i regionalnych. W kontekście aplikacji webowych, internacjonalizacja obejmuje przekład tekstów, formatowanie dat, liczb oraz adaptację do lokalnych zwyczajów i prawnych wymogów. Wdrażanie internacjonalizacji we frameworku Next.js może być znacząco uproszczone dzięki wykorzystaniu biblioteki next-intl
. Poniżej przedstawiam krok po kroku, jak zintegrować next-intl
z aplikacją Next.js, by obsługiwać routing i18n.
Wdrażamy i18n tak aby język był widoczny w urlu.
Opis jest tworzony dla wersji:
"next": "14.2.3",
"next-intl": "^3.17.2",
Krok 1: Instalacja biblioteki
npm install next-intl
Krok 2: Struktura katalogów
├── i18n
│ ├── messages (1)
│ │ ├──en.json
│ │ └──pl.json
│ │
│ ├── config.ts (2)
│ ├── i18n.ts (3)
│ └── navigation.ts (4)
│
├── middleware.ts (5)
├── next.config.mjs (6)
│
└── app
└──[locale]
├── layout.tsx (7)
├── login
│ └── page.tsx (8)
│
└── dashboard
└── page.tsx (8)
Katalog (1) – messages
Zawiera pliki z tłumaczeniami, przykładowa zawartość:
{
"DashboardPage": {
"title": "Dashboard",
"chatTitle": "Cost",
"tableTitle": "Last users"
},
"LoginForm": {
"title": "Sign in",
"rememberMe": "Remember me",
"error": "Invalid email or password",
"signUp": "Don't have an account? Sign Up"
}
}
Plik (2) – config.ts
W moim przypadku jest to tylko deklaracja języków dostępnych w aplikacji
export const defaultLocale = 'en' as const;
export const locales = ['en', 'pl'] as const;
Plik (3) – i18n.ts
Plik odpowiedzialny za ładowanie tłumaczeń
import { notFound } from 'next/navigation';
import { getRequestConfig } from 'next-intl/server';
import { locales } from './config';
export default getRequestConfig(async ({locale}) => {
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`./messages/${locale}.json`)).default
};
});
Plik (4) – navigation.ts
Plik zwracająca hooki i komponenty, które pomagają w zarządzaniu lokalizacjami i ścieżkami.
import {createSharedPathnamesNavigation} from 'next-intl/navigation';
import {locales} from './config';
export const {Link, redirect, usePathname, useRouter} = createSharedPathnamesNavigation({locales});
Link
Komponent Link
dostarczany przez createSharedPathnamesNavigation
jest rozszerzeniem standardowego komponentu Link
z Next.js, ale dodatkowo automatycznie obsługuje lokalizacje. Umożliwia tworzenie linków, które są świadome kontekstu językowego, automatycznie dodając odpowiedni prefix językowy do ścieżek.
<Link href="/about">
<a>About Us</a>
</Link>
redirect
Funkcja redirect
służy do programowego przekierowania użytkownika na inną stronę z uwzględnieniem aktualnie wybranej lokalizacji. Działa podobnie do funkcji router.push
lub router.replace
z Next.js, ale dodaje obsługę lokalizacji.
redirect('/dashboard');
usePathname
Hook usePathname
jest używany do odczytania aktualnej ścieżki bez prefixu lokalizacji. Jest to przydatne, gdy potrzebujesz operować na URL bez uwzględniania aktualnej lokalizacji użytkownika.
const pathname = usePathname();
console.log(pathname); // Wyświetla ścieżkę bez prefixu językowego
useRouter
useRouter
zwracany przez createSharedPathnamesNavigation
to rozszerzona wersja hooka useRouter
z Next.js, która dodatkowo uwzględnia internacjonalizację. Dzięki temu, oprócz standardowych funkcji routera, zapewnia łatwy dostęp do funkcji związanych z lokalizacją, takich jak zmiana języka.
const router = useRouter();
router.push('/contact'); // Automatycznie dodaje prefix lokalizacji
Plik (5) – middleware.ts
W pliku mam już obsłużoną NextAuth.js, także muszę obsłużyć autoryzacje oraz i18n:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// Importowanie funkcji do tworzenia middleware obsługującego internacjonalizację
import createMiddleware from 'next-intl/middleware';
// Importowanie funkcji do tworzenia middleware obsługującego autoryzację z next-auth
import { withAuth } from 'next-auth/middleware';
// Importowanie konfiguracji języków i domyślnego języka
import { locales, defaultLocale } from './i18n/config';
// Tworzenie middleware do internacjonalizacji z konfiguracją języków
const intlMiddleware = createMiddleware({
locales,
defaultLocale: defaultLocale
});
// Definiowanie ścieżek stron dostępnych publicznie, które nie wymagają autoryzacji
const publicPages = ["/login", "/signin", "/signup", "/auth/signout", "/api/auth/callback"];
// Tworzenie middleware do autoryzacji z zastosowaniem intlMiddleware
const authMiddleware = withAuth(
(req) => intlMiddleware(req),
{
callbacks: {
authorized: ({token}) => token != null // Sprawdzanie, czy użytkownik posiada ważny token
},
pages: {
signIn: '/login' // Definiowanie ścieżki do strony logowania
}
}
);
// Główna funkcja middleware, która decyduje, czy użyć intlMiddleware czy authMiddleware
export default async function middleware(req: NextRequest) {
// Tworzenie wyrażenia regularnego do sprawdzenia, czy ścieżka jest publiczna
const publicPathnameRegex = RegExp(`^(/(${locales.join('|')}))?(${publicPages.flatMap(p => (p === '/' ? ['', '/'] : p)).join('|')})/?$`, 'i');
// Sprawdzanie, czy ścieżka jest stroną publiczną
const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname);
if (isPublicPage) {
// Jeśli strona jest publiczna, stosujemy tylko intlMiddleware
return intlMiddleware(req);
} else {
// Jeśli strona nie jest publiczna, stosujemy autoryzację a następnie intlMiddleware
return (authMiddleware as any)(req);
}
}
// Konfiguracja ścieżek, które mają być obsługiwane przez middleware
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$|.*\\.webp$).*)", '/(pl|en)/:path*'],
};
Plik (6) – next.config.mjs
Plik konfiguracyjny Next.js z dodanym pluginem next-intl
/** @type {import('next').NextConfig} */
// Importowanie funkcji tworzącej plugin internacjonalizacji z biblioteki next-intl
import createNextIntlPlugin from "next-intl/plugin";
// Tworzenie instancji pluginu next-intl, wskazanie pliku z konfiguracją i18n
const withNextIntl = createNextIntlPlugin("./i18n/i18n.ts");
// Definiowanie głównej konfiguracji Next.js
const nextConfig = {
...
};
// Eksportowanie skonfigurowanego obiektu nextConfig za pomocą pluginu next-intl
export default withNextIntl(nextConfig);
Plik (7) – layout.tsx
Plik z RootLayout gdzie dodajemy komponent NextIntlClientProvider, dzięki czemu zapewnimy, że cała aplikacja będzie miała dostęp do funkcji umożliwiających wyświetlanie treści w odpowiednim języku. Dzięki temu, niezależnie od tego, gdzie użytkownik znajduje się w aplikacji, wszystko będzie mogło być automatycznie dostosowane do jego języka i regionu, co poprawia ogólną użyteczność i dostępność aplikacji.
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from "next-intl/server";
export default async function LocaleLayout({
children
}: {
children: React.ReactNode;
}) {
const messages = await getMessages();
const locale = await getLocale();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Pliki (8) – page.tsx
Pliki konkretnej ścieżki, gdzie możemy wykorzystać tłumaczenie:
import { useTranslations } from "next-intl";
export default function DashboardPage() {
//podajamy klucz z jsona
const t = useTranslations("DashboardPage");
return (
<>
{/* ..... */}
<h1>{t("chatTitle")}</h1>
{/* ..... */}
<h1>{t("tableTitle")}</h1>
{/* ..... */}
<h1>{t("title")}</h1>
{/* ..... */}
</>
);
}
Selekt do zmiany języka
import * as React from "react";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import { useRouter, usePathname } from "@/i18n/navigation";
import { locales } from "@/i18n/config";
const SelectLanguage = () => {
const router = useRouter();
const pathname = usePathname();
const locale = router.locale;
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const newLocale = event.target.value as string;
router.replace(pathname, { locale: newLocale });
};
return (
<Select value={locale} onChange={handleChange} displayEmpty inputProps={{ "aria-label": "Without label" }}>
{locales.map((localeOption) => (
<MenuItem key={localeOption} value={localeOption}>
{localeOption.toUpperCase()}
</MenuItem>
))}
</Select>
);
};
export default SelectLanguage;