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});

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;

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *