Customize Consent Preferences

We use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.

The cookies that are categorized as "Necessary" are stored on your browser as they are essential for enabling the basic functionalities of the site. ... 

Always Active

Necessary cookies are required to enable the basic features of this site, such as providing secure log-in or adjusting your consent preferences. These cookies do not store any personally identifiable data.

No cookies to display.

Functional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features.

No cookies to display.

Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics such as the number of visitors, bounce rate, traffic source, etc.

No cookies to display.

Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.

No cookies to display.

Advertisement cookies are used to provide visitors with customized advertisements based on the pages you visited previously and to analyze the effectiveness of the ad campaigns.

No cookies to display.

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 *