Strona głównaPiaskownicaPrezentacjaAplikacjaDokumentacjaBlog
    • Englishangielski
      EN
    • русскийrosyjski
      RU
    • 日本語japoński
      JA
    • françaisfrancuski
      FR
    • 한국어koreański
      KO
    • 中文chiński
      ZH
    • españolhiszpański
      ES
    • Deutschniemiecki
      DE
    • العربيةarabski
      AR
    • italianowłoski
      IT
    • British Englishangielski brytyjski
      EN-GB
    • portuguêsportugalski
      PT
    • हिन्दीhindi
      HI
    • Türkçeturecki
      TR
    • polskipolski
      PL
    • Indonesiaindonezyjski
      ID
    • Tiếng Việtwietnamski
      VI
    • українськаukraiński
      UK
    /
    Alt+←
    Co to jest internacjonalizacja (i18n)?
    SEO dan i18n
    Przewodnik
    • i18n przy użyciu next-i18next
    • i18n przy użyciu next-intl
    Użyj Intlayer w swoim rozwiązaniu
    • Automatyzacja next-i18next
    • Automatyzacja react-i18next
    • Automatyzacja next-intl
    • Automatyzacja react-intl
    • Automatyzacja vue-i18n
    Porównania
    • next-i18next vs next-intl vs Intlayer
    • react-i18next vs react-intl vs Intlayer
    Dokumentacja
    1. Blog
    2. Next i18next vs next intl vs intlayer
    Data utworzenia:2025-08-23Ostatnia aktualizacja:2025-09-29
    Prześlij ten dokument do swojego ulubionego asystenta AI
    ChatGPT
    Claude
    DeepSeek
    Google AI mode
    Gemini
    Perplexity
    Mistral
    Grok

    Zadaj pytanie i otrzymaj streszczenie dokumentu, odwołując się do tej strony i wybranego dostawcy AI

    Treść tej strony została przetłumaczona przy użyciu sztucznej inteligencji.

    Zobacz ostatnią wersję oryginalnej treści w języku angielskim
    Edytuj tę dokumentację

    Jeśli masz pomysł na ulepszenie tej dokumentacji, zachęcamy do przesłania pull requesta na GitHubie.

    Link do dokumentacji na GitHubie
    Kopiuj

    Kopiuj dokument Markdown do schowka

    next-i18next VS next-intl VS intlayer | Internacjonalizacja (i18n) Next.js

    next-i18next VS next-intl VS intlayer

    Przyjrzyjmy się podobieństwom i różnicom między trzema opcjami i18n dla Next.js: next-i18next, next-intl oraz Intlayer.

    To nie jest pełny poradnik. To porównanie, które ma pomóc Ci w wyborze.

    Skupiamy się na Next.js 13+ App Router (z React Server Components) i oceniamy:

    w skrócie: Wszystkie trzy rozwiązania mogą lokalizować aplikację Next.js. Jeśli chcesz zawartość ograniczoną do komponentu, ścisłe typy TypeScript, sprawdzanie brakujących kluczy podczas kompilacji, słowniki poddane tree-shakingowi oraz pierwszorzędne wsparcie App Router + SEO, Intlayer jest najbardziej kompletnym i nowoczesnym wyborem.
    Częstym nieporozumieniem wśród deweloperów jest myślenie, że next-intl to wersja Next.js react-intl. Tak nie jest, next-intl jest utrzymywany przez Amann, podczas gdy react-intl jest utrzymywany przez FormatJS.

    W skrócie

    • next-intl - Lekki, prosty formatowanie wiadomości z solidnym wsparciem Next.js. Centralizowane katalogi są powszechne; DX jest prosty, ale bezpieczeństwo i utrzymanie na dużą skalę pozostają głównie Twoją odpowiedzialnością.
    • next-i18next - i18next w oprawie Next.js. Dojrzały ekosystem i funkcje dostępne przez wtyczki (np. ICU), ale konfiguracja może być rozbudowana, a katalogi mają tendencję do centralizacji wraz z rozwojem projektów.
    • Intlayer - Model zawartości skoncentrowany na komponentach dla Next.js, ścisłe typowanie TS, sprawdzanie podczas kompilacji, tree-shaking, wbudowane middleware i pomocniki SEO, opcjonalny Visual Editor/CMS oraz tłumaczenia wspomagane przez AI.

    Pokaż całą zawartość tabeli

    Otwórz tabelę w oknie modalnym, aby wyraźnie zobaczyć całą zawartość

    Library GitHub Stars Total Commits Last Commit First Version NPM Version NPM Downloads
    aymericzip/intlayer GitHub Repo stars GitHub commit activity Last Commit April 2024 npm npm downloads
    amannn/next-intl GitHub Repo stars GitHub commit activity Last Commit Nov 2020 npm npm downloads
    i18next/i18next GitHub Repo stars GitHub commit activity Last Commit Jan 2012 npm npm downloads
    i18next/next-i18next GitHub Repo stars GitHub commit activity Last Commit Nov 2018 npm npm downloads
    Odznaki aktualizują się automatycznie. Zrzuty ekranu mogą się zmieniać w czasie.

    Porównanie funkcji obok siebie (skoncentrowane na Next.js)

    Pokaż całą zawartość tabeli

    Otwórz tabelę w oknie modalnym, aby wyraźnie zobaczyć całą zawartość

    Funkcja next-intlayer (Intlayer) next-intl next-i18next
    Tłumaczenia blisko komponentów ✅ Tak, zawartość zlokalizowana razem z każdym komponentem ❌ Nie ❌ Nie
    Integracja z TypeScript ✅ Zaawansowana, automatycznie generowane ścisłe typy ✅ Dobra ⚠️ Podstawowa
    Wykrywanie brakujących tłumaczeń ✅ Podświetlanie błędów TypeScript oraz błędy/ostrzeżenia podczas kompilacji ⚠️ Obsługa awaryjna w czasie wykonywania ⚠️ Obsługa awaryjna w czasie wykonywania
    Bogata zawartość (JSX/Markdown/komponenty) ✅ Bezpośrednie wsparcie ❌ Nie zaprojektowane dla bogatych węzłów ⚠️ Ograniczone
    Tłumaczenie wspomagane AI ✅ Tak, obsługuje wielu dostawców AI. Można używać własnych kluczy API. Uwzględnia kontekst Twojej aplikacji i zakres treści ❌ Nie ❌ Nie
    Edytor wizualny ✅ Tak, lokalny Edytor wizualny + opcjonalny CMS; może eksternalizować zawartość codebase; osadzalny ❌ Nie / dostępny przez zewnętrzne platformy lokalizacyjne ❌ Nie / dostępny przez zewnętrzne platformy lokalizacyjne
    Lokalizowane routingi ✅ Tak, obsługuje lokalizowane ścieżki od razu (działa z Next.js i Vite) ✅ Wbudowany, App Router obsługuje segment [locale] ✅ Wbudowany
    Generowanie dynamicznych tras ✅ Tak ✅ Tak ✅ Tak
    Pluralizacja ✅ Wzorce oparte na enumeracji ✅ Dobre ✅ Dobre
    Formatowanie (daty, liczby, waluty) ✅ Zoptymalizowane formatery (Intl w tle) ✅ Dobre (pomocniki Intl) ✅ Dobre (pomocniki Intl)
    Format treści ✅ .tsx, .ts, .js, .json, .md, .txt, (.yaml WIP) ✅ .json, .js, .ts ⚠️ .json
    Wsparcie ICU ⚠️ W trakcie realizacji ✅ Tak ⚠️ Za pomocą wtyczki (i18next-icu)
    Narzędzia SEO (hreflang, sitemap) ✅ Wbudowane narzędzia: pomocniki do sitemap, robots.txt, metadanych ✅ Dobre ✅ Dobre
    Ekosystem / Społeczność ⚠️ Mniejsza, ale szybko rosnąca i reagująca ✅ Dobra ✅ Dobra
    Renderowanie po stronie serwera i komponenty serwerowe ✅ Tak, zoptymalizowane pod SSR / React Server Components ⚠️ Obsługiwane na poziomie strony, ale wymaga przekazywania funkcji t w drzewie komponentów dla dzieci komponentów serwerowych ⚠️ Obsługiwane na poziomie strony, ale wymaga przekazywania funkcji t w drzewie komponentów dla dzieci komponentów serwerowych
    Tree-shaking (ładowanie tylko używanej zawartości) ✅ Tak, per-komponent podczas budowania za pomocą wtyczek Babel/SWC ⚠️ Częściowo ⚠️ Częściowo
    Lazy loading (leniwe ładowanie) ✅ Tak, per-lokalizacja / per-słownik ✅ Tak (per-trasa/per-lokalizacja), wymaga zarządzania przestrzeniami nazw ✅ Tak (per-trasa/per-lokalizacja), wymaga zarządzania przestrzeniami nazw
    Usuwanie nieużywanych treści ✅ Tak, na poziomie słownika podczas budowania ❌ Nie, można zarządzać ręcznie za pomocą zarządzania przestrzenią nazw ❌ Nie, można zarządzać ręcznie za pomocą zarządzania przestrzenią nazw
    Zarządzanie dużymi projektami ✅ Zachęca do modularności, odpowiednie dla systemów projektowych ✅ Modularne z konfiguracją ✅ Modularne z konfiguracją
    Testowanie brakujących tłumaczeń (CLI/CI) ✅ CLI: npx intlayer content test (audyt przyjazny CI) ⚠️ Nie wbudowane; dokumentacja sugeruje npx @lingual/i18n-check ⚠️ Nie wbudowane; opiera się na narzędziach i18next / runtime saveMissing

    Wprowadzenie

    Next.js oferuje wbudowane wsparcie dla internacjonalizowanego routingu (np. segmenty lokalizacji). Jednak ta funkcja sama w sobie nie wykonuje tłumaczeń. Nadal potrzebujesz biblioteki do renderowania zlokalizowanych treści dla użytkowników.

    Istnieje wiele bibliotek i18n, ale w świecie Next.js obecnie trzy zyskują na popularności: next-i18next, next-intl oraz Intlayer.


    Architektura i skalowalność

    • next-intl / next-i18next: Domyślnie korzystają z centralizowanych katalogów dla każdego locale (oraz namespaces w i18next). Działa dobrze na początku, ale często staje się dużą wspólną powierzchnią z rosnącym sprzężeniem i zmianami kluczy.
    • Intlayer: Zachęca do stosowania słowników per-komponent (lub per-funkcjonalność) współlokowanych z kodem, który obsługują. Zmniejsza to obciążenie poznawcze, ułatwia duplikację/migrację elementów UI oraz redukuje konflikty między zespołami. Nieużywana zawartość jest naturalnie łatwiejsza do wykrycia i usunięcia.

    Dlaczego to ważne: W dużych bazach kodu lub systemach designu, modułowa zawartość skalują się lepiej niż monolityczne katalogi.


    Rozmiary pakietów i zależności

    Po zbudowaniu aplikacji, bundle to JavaScript, który przeglądarka załaduje, aby wyrenderować stronę. Rozmiar bundle jest więc istotny dla wydajności aplikacji.

    Dwa komponenty są ważne w kontekście bundle aplikacji wielojęzycznej:

    • Kod aplikacji
    • Zawartość ładowana przez przeglądarkę

    Kod aplikacji

    Znaczenie kodu aplikacji jest w tym przypadku minimalne. Wszystkie trzy rozwiązania są tree-shakable, co oznacza, że nieużywane części kodu nie są dołączane do bundle.

    Oto porównanie rozmiaru bundle JavaScript ładowanego przez przeglądarkę dla aplikacji wielojęzycznej z trzema rozwiązaniami.

    Jeśli w aplikacji nie potrzebujemy żadnego formatera, lista eksportowanych funkcji po tree-shakingu będzie następująca:

    • next-intlayer: useIntlayer, useLocale, NextIntlClientProvider, (Rozmiar bundla to 180,6 kB -> 78,6 kB (gzip))
    • next-intl: useTranslations, useLocale, NextIntlClientProvider, (Rozmiar bundla to 101,3 kB -> 31,4 kB (gzip))
    • next-i18next: useTranslation, useI18n, I18nextProvider, (Rozmiar bundla to 80,7 kB -> 25,5 kB (gzip))

    Te funkcje są jedynie wrapperami wokół kontekstu/stanu React, więc całkowity wpływ biblioteki i18n na rozmiar bundla jest minimalny.

    Intlayer jest nieco większy niż next-intl i next-i18next, ponieważ zawiera więcej logiki w funkcji useIntlayer. Jest to związane z integracją markdown i intlayer-editor.

    Zawartość i tłumaczenia

    Ta część jest często ignorowana przez deweloperów, ale rozważmy przypadek aplikacji składającej się z 10 stron w 10 językach. Załóżmy, że każda strona zawiera w 100% unikalną treść, aby uprościć obliczenia (w rzeczywistości wiele treści jest powtarzalnych między stronami, np. tytuł strony, nagłówek, stopka itp.).

    Użytkownik chcący odwiedzić stronę /fr/about załaduje zawartość jednej strony w danym języku. Ignorowanie optymalizacji treści oznaczałoby niepotrzebne załadowanie 8 200% ((1 + (((10 stron - 1) × (10 języków - 1)))) × 100) zawartości aplikacji. Widzisz problem? Nawet jeśli ta zawartość pozostaje tekstem, a Ty prawdopodobnie wolisz myśleć o optymalizacji obrazów na swojej stronie, wysyłasz niepotrzebną treść na cały świat i zmuszasz komputery użytkowników do jej przetwarzania bez powodu.

    Dwa ważne zagadnienia:

    • Podział według ścieżki:

      Jeśli jestem na stronie /about, nie chcę ładować zawartości strony /home
    • Podział według lokalizacji:

      Jeśli jestem na stronie /fr/about, nie chcę ładować zawartości strony /en/about

    Ponownie, wszystkie trzy rozwiązania zdają sobie sprawę z tych problemów i pozwalają zarządzać tymi optymalizacjami. Różnica między tymi trzema rozwiązaniami to DX (Developer Experience).

    next-intl i next-i18next używają scentralizowanego podejścia do zarządzania tłumaczeniami, pozwalając na podział plików JSON według lokalizacji i podplików. W next-i18next nazywamy pliki JSON 'namespaces'; next-intl pozwala deklarować wiadomości. W intlayer nazywamy pliki JSON 'dictionaries'.

    • W przypadku next-intl, podobnie jak w next-i18next, zawartość jest ładowana na poziomie strony/layoutu, a następnie ta zawartość jest ładowana do providera kontekstu. Oznacza to, że deweloper musi ręcznie zarządzać plikami JSON, które będą ładowane dla każdej strony.
    W praktyce oznacza to, że deweloperzy często pomijają tę optymalizację, woląc dla uproszczenia załadować całą zawartość w providerze kontekstu strony.
    • W przypadku intlayer cała zawartość jest ładowana w aplikacji. Następnie wtyczka (@intlayer/babel / @intlayer/swc) zajmuje się optymalizacją bundla, ładując tylko zawartość używaną na stronie. Deweloper nie musi więc ręcznie zarządzać słownikami, które będą ładowane. Pozwala to na lepszą optymalizację, lepszą utrzymywalność oraz skraca czas developmentu.

    W miarę rozwoju aplikacji (szczególnie gdy nad aplikacją pracuje wielu programistów), często zdarza się zapomnieć o usunięciu treści, które nie są już używane, z plików JSON.

    Należy zauważyć, że wszystkie pliki JSON są ładowane we wszystkich przypadkach (next-intl, next-i18next, intlayer).

    Dlatego podejście Intlayer jest bardziej wydajne: jeśli komponent nie jest już używany, jego słownik nie jest ładowany do bundla.

    Równie ważne jest, jak biblioteka obsługuje mechanizm fallbacków. Załóżmy, że aplikacja domyślnie jest w języku angielskim, a użytkownik odwiedza stronę /fr/about. Jeśli tłumaczenia we francuskim są niekompletne, zostanie zastosowany fallback na angielski.

    W przypadku next-intl i next-i18next biblioteka wymaga załadowania pliku JSON związanego z bieżącym locale, ale także z locale zapasowym (fallback). W związku z tym, zakładając, że cały content został przetłumaczony, każda strona załaduje 100% niepotrzebnej zawartości. Dla porównania, intlayer przetwarza fallback podczas budowania słownika. W ten sposób każda strona załaduje tylko używaną zawartość.

    Uwaga: Aby zoptymalizować bundle za pomocą intlayer, musisz ustawić opcję importMode: 'dynamic' w pliku intlayer.config.ts. I upewnić się, że wtyczka @intlayer/babel / @intlayer/swc jest zainstalowana (domyślnie instalowana przy użyciu vite-intlayer).

    Oto przykład wpływu optymalizacji rozmiaru bundle za pomocą intlayer w aplikacji vite + react:

    Pokaż całą zawartość tabeli

    Otwórz tabelę w oknie modalnym, aby wyraźnie zobaczyć całą zawartość

    Zoptymalizowany pakiet Pakiet niezoptymalizowany
    zoptymalizowany pakiet niezoptymalizowany pakiet

    TypeScript i bezpieczeństwo

    next-i18next

    • Podstawowe typy dla hooków. ścisłe typowanie kluczy wymaga dodatkowych narzędzi/konfiguracji.

    next-intl

    • Solidne wsparcie TypeScript, ale klucze nie są domyślnie ściśle typowane. Bezpieczeństwo utrzymasz ręcznie.

    intlayer

    • Generuje ścisłe typy na podstawie twoich treści. Autouzupełnianie w IDE oraz błędy podczas kompilacji wychwytują literówki i brakujące klucze przed wdrożeniem.

    Dlaczego to ważne: Silne typowanie przesuwa błędy na wcześniejszy etap (CI/build), zamiast na późniejszy (czas działania).


    Obsługa brakujących tłumaczeń

    next-i18next

    • Polega na fallbackach w czasie działania. Budowanie nie kończy się błędem.

    next-intl

    • Polega na fallbackach w czasie działania. Budowanie nie kończy się błędem.

    intlayer

    • Wykrywanie podczas kompilacji z ostrzeżeniami/błędami dla brakujących lokalizacji lub kluczy.

    Dlaczego to ważne: Wykrywanie braków podczas kompilacji zapobiega pojawianiu się 'undefined' w produkcji.


    Routing, middleware i strategia URL

    next-i18next

    • Umożliwia lokalizowany routing. Jednak middleware nie jest wbudowane.

    next-intl

    • Umożliwia lokalizowany routing.
    • Dostarcza middleware.

    intlayer

    • Umożliwia lokalizowany routing.
    • Dostarcza middleware.

    Dlaczego to ważne: Pomaga w SEO i odkrywalności, a także poprawia doświadczenie użytkownika.


    Wsparcie dla Server Components (RSC)

    next-i18next

    • Wspiera komponenty serwerowe stron i layoutów.
    • Nie zapewnia synchronicznego API dla dziecięcych komponentów serwerowych.

    next-intl

    • Obsługuje komponenty serwerowe stron i układów.
    • Nie zapewnia synchronicznego API dla dziecięcych komponentów serwerowych.

    intlayer

    • Obsługuje komponenty serwerowe stron i układów.
    • Zapewnia synchroniczne API dla dziecięcych komponentów serwerowych.

    Dlaczego to ważne: Wsparcie komponentów serwerowych jest kluczową funkcją Next.js 13+, wspomagającą wydajność. Przekazywanie propsów takich jak locale lub funkcji t z komponentu nadrzędnego do dziecięcych komponentów serwerowych sprawia, że Twoje komponenty są mniej wielokrotnego użytku.


    Integracja z platformami lokalizacyjnymi (TMS)

    Duże organizacje często polegają na Systemach Zarządzania Tłumaczeniami (TMS), takich jak Crowdin, Phrase, Lokalise, Localizely czy Localazy.

    • Dlaczego firmy zwracają na to uwagę

      • Współpraca i role: Zaangażowanych jest wielu uczestników: deweloperzy, menedżerowie produktu, tłumacze, recenzenci, zespoły marketingowe.
      • Skala i efektywność: ciągła lokalizacja, przegląd w kontekście.
    • next-intl / next-i18next

      • Zazwyczaj używają scentralizowanych katalogów JSON, więc eksport/import z TMS jest prosty.
      • Dojrzałe ekosystemy oraz przykłady/integracje dla wymienionych platform.
    • Intlayer

      • Zachęca do zdecentralizowanych słowników per komponent i obsługuje zawartość w formatach TypeScript/TSX/JS/JSON/MD.
      • Poprawia to modularność w kodzie, ale może utrudniać integrację typu plug-and-play z TMS, gdy narzędzie oczekuje scentralizowanych, płaskich plików JSON.
      • Intlayer oferuje alternatywy: tłumaczenia wspomagane AI (z użyciem własnych kluczy dostawcy), Edytor Wizualny/CMS oraz workflowy CLI/CI do wykrywania i wypełniania luk.
    Uwaga: next-intl i i18next również akceptują katalogi TypeScript. Jeśli Twój zespół przechowuje wiadomości w plikach .ts lub decentralizuje je według funkcji, możesz napotkać podobne problemy z TMS. Jednak wiele konfiguracji next-intl pozostaje scentralizowanych w folderze locales/, co jest nieco łatwiejsze do przekształcenia na JSON dla TMS.

    Doświadczenie dewelopera

    Ta część dokonuje dogłębnego porównania trzech rozwiązań. Zamiast rozważać proste przypadki, opisane w dokumentacji „getting started” dla każdego rozwiązania, rozważymy rzeczywisty przypadek użycia, bardziej zbliżony do prawdziwego projektu.

    Struktura aplikacji

    Struktura aplikacji jest ważna, aby zapewnić dobrą utrzymywalność Twojej bazy kodu.

    bash
    Kopiuj kod

    Skopiuj kod do schowka

    .├── i18n.config.ts└── src  ├── locales  │   ├── en  │   │  ├── common.json  │   │  └── about.json  │   └── fr  │      ├── common.json  │      └── about.json  ├── app  │   ├── i18n  │   │   └── server.ts  │   └── [locale]  │       ├── layout.tsx  │       └── about.tsx  └── components      ├── I18nProvider.tsx      ├── ClientComponent.tsx      └── ServerComponent.tsx
    bash
    Kopiuj kod

    Skopiuj kod do schowka

    .├── i18n.ts├── locales│   ├── en│   │  ├── home.json│   │  └── navbar.json│   ├── fr│   │  ├── home.json│   │  └── navbar.json│   └── es│      ├── home.json│      └── navbar.json└── src  ├── middleware.ts  ├── app  │   ├── i18n  │   │   └── server.ts  │   └── [locale]  │       └── home.tsx  └── components      └── Navbar          └── index.tsx
    bash
    Kopiuj kod

    Skopiuj kod do schowka

    .├── intlayer.config.ts└── src  ├── middleware.ts  ├── app  │   └── [locale]  │       ├── layout.tsx  │       └── home  │           ├── index.tsx  │           └── index.content.ts  └── components      └── Navbar          ├── index.tsx          └── index.content.ts

    Porównanie

    • next-intl / next-i18next: Centralizowane katalogi (JSON; przestrzenie nazw/wiadomości). Jasna struktura, dobrze integruje się z platformami tłumaczeniowymi, ale może prowadzić do większej liczby edycji między plikami w miarę rozwoju aplikacji.
    • Intlayer: Słowniki .content.{ts|js|json} przypisane do komponentów i umieszczone razem z nimi. Ułatwia ponowne użycie komponentów i lokalne rozumowanie; dodaje pliki i opiera się na narzędziach działających podczas budowania.

    Konfiguracja i ładowanie zawartości

    Jak wspomniano wcześniej, musisz zoptymalizować sposób importowania każdego pliku JSON do swojego kodu. Sposób, w jaki biblioteka obsługuje ładowanie zawartości, jest ważny.

    i18n.config.ts
    Kopiuj kod

    Skopiuj kod do schowka

    export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export const rtlLocales = ["ar", "he", "fa", "ur"] as const;export const isRtl = (locale: string) =>(rtlLocales as readonly string[]).includes(locale);// Funkcja zwracająca ścieżkę z uwzględnieniem lokalizacjiexport function localizedPath(locale: string, path: string) {return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";// Funkcja zwracająca absolutny URL z lokalizacjąexport function abs(locale: string, path: string) {return ORIGIN + localizedPath(locale, path);}
    src/app/i18n/server.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";// Ładowanie zasobów JSON z src/locales/<locale>/<namespace>.jsonconst backend = resourcesToBackend((locale: string, namespace: string) =>  import(`../../locales/${locale}/${namespace}.json`));export async function initI18next(locale: string,namespaces: string[] = ["common"]) {const i18n = createInstance();await i18n  .use(initReactI18next)  .use(backend)  .init({    lng: locale,    fallbackLng: defaultLocale,    ns: namespaces,    defaultNS: "common",    interpolation: { escapeValue: false },    react: { useSuspense: false },  });return i18n;}
    src/components/I18nProvider.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    "use client";import * as React from "react";import { I18nextProvider } from "react-i18next";import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";const backend = resourcesToBackend((locale: string, namespace: string) =>  import(`../../locales/${locale}/${namespace}.json`));type Props = {locale: string;namespaces?: string[];resources?: Record<string, any>; // { ns: pakiet }children: React.ReactNode;};export default function I18nProvider({locale,namespaces = ["common"],resources,children,}: Props) {const [i18n] = React.useState(() => {  const i = createInstance();  i.use(initReactI18next)    .use(backend)    .init({      lng: locale,      fallbackLng: defaultLocale,      ns: namespaces,      resources: resources ? { [locale]: resources } : undefined,      defaultNS: "common",      interpolation: { escapeValue: false },      react: { useSuspense: false },    });  return i;});return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;}
    src/app/[locale]/layout.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import type { ReactNode } from "react";import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";export const dynamicParams = false;export function generateStaticParams() {return locales.map((locale) => ({ locale }));}export default function LocaleLayout({children,params,}: {children: ReactNode;params: { locale: string };}) {const locale: Locale = (locales as readonly string[]).includes(params.locale)  ? params.locale  : defaultLocale;const dir = isRtl(locale) ? "rtl" : "ltr";return (  <html lang={locale} dir={dir}>    <body>{children}</body>  </html>);}
    src/app/[locale]/about.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import I18nProvider from "@/components/I18nProvider";import { initI18next } from "@/app/i18n/server";import type { Locale } from "@/i18n.config";import ClientComponent from "@/components/ClientComponent";import ServerComponent from "@/components/ServerComponent";// Wymuś statyczne renderowanie stronyexport const dynamic = "force-static";export default async function AboutPage({params: { locale },}: {params: { locale: Locale };}) {const namespaces = ["common", "about"] as const;const i18n = await initI18next(locale, [...namespaces]);const tAbout = i18n.getFixedT(locale, "about");return (  <I18nProvider locale={locale} namespaces={[...namespaces]}>    <main>      <h1>{tAbout("title")}</h1>      <ClientComponent />      <ServerComponent t={tAbout} locale={locale} count={0} />    </main>  </I18nProvider>);}
    src/i18n.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";export const locales = ["en", "fr", "es"] as const;export const defaultLocale = "en" as const;async function loadMessages(locale: string) {// Załaduj tylko przestrzenie nazw potrzebne dla twojego layoutu/stronconst [common, about] = await Promise.all([  import(`../locales/${locale}/common.json`).then((m) => m.default),  import(`../locales/${locale}/about.json`).then((m) => m.default),]);return { common, about } as const;}export default getRequestConfig(async ({ locale }) => {if (!locales.includes(locale)) notFound();return {  messages: await loadMessages(locale),};});
    src/app/[locale]/layout.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import type { ReactNode } from "react";import { locales } from "@/i18n";import {getLocaleDirection,unstable_setRequestLocale,} from "next-intl/server";export const dynamic = "force-static";export function generateStaticParams() {return locales.map((locale) => ({ locale }));}export default async function LocaleLayout({children,params,}: {children: ReactNode;params: Promise<{ locale: string }>;}) {const { locale } = await params;// Ustaw aktywny język żądania dla tego renderowania po stronie serwera (RSC)unstable_setRequestLocale(locale);const dir = getLocaleDirection(locale);return (  <html lang={locale} dir={dir}>    <body>{children}</body>  </html>);}
    src/app/[locale]/about/page.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponentExample from "@/components/ClientComponentExample";export const dynamic = "force-static";export default async function AboutPage({params,}: {params: Promise<{ locale: string }>;}) {const { locale } = await params;// Wiadomości są ładowane po stronie serwera. Przekaż tylko to, co jest potrzebne klientowi.const messages = await getMessages();const clientMessages = pick(messages, ["common", "about"]);// Tłumaczenia/formatowanie wyłącznie po stronie serweraconst tAbout = await getTranslations("about");const tCounter = await getTranslations("about.counter");const format = await getFormatter();const initialFormattedCount = format.number(0);return (  <NextIntlClientProvider locale={locale} messages={clientMessages}>    <main>      <h1>{tAbout("title")}</h1>      <ClientComponentExample />      <ServerComponent        formattedCount={initialFormattedCount}        label={tCounter("label")}        increment={tCounter("increment")}      />    </main>  </NextIntlClientProvider>);}
    intlayer.config.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import { type IntlayerConfig, Locales } from "intlayer";const config: IntlayerConfig = {internationalization: {  locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],  defaultLocale: Locales.ENGLISH,},};export default config;
    src/app/[locale]/layout.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import { getHTMLTextDir } from "intlayer";import {IntlayerClientProvider,generateStaticParams,type NextLayoutIntlayer,} from "next-intlayer";export const dynamic = "force-static";const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {const { locale } = await params;return (  <html lang={locale} dir={getHTMLTextDir(locale)}>    <body>      <IntlayerClientProvider locale={locale}>        {children}      </IntlayerClientProvider>    </body>  </html>);};export default LandingLayout;
    src/app/[locale]/about/page.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import { PageContent } from "@components/PageContent";import type { NextPageIntlayer } from "next-intlayer";import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";import { ClientComponent, ServerComponent } from "@components";const LandingPage: NextPageIntlayer = async ({ params }) => {const { locale } = await params;const { title } = useIntlayer("about", locale);return (  <IntlayerServerProvider locale={locale}>    <main>      <h1>{title}</h1>      <ClientComponent />      <ServerComponent />    </main>  </IntlayerServerProvider>);};export default LandingPage;

    Porównanie

    Wszystkie trzy wspierają ładowanie treści i providerów dla poszczególnych lokalizacji.

    • W przypadku next-intl/next-i18next zazwyczaj ładujesz wybrane wiadomości/przestrzenie nazw na trasę i umieszczasz providery tam, gdzie są potrzebne.

    • W przypadku Intlayer dodaje analizę w czasie budowania, aby wywnioskować użycie, co może zmniejszyć ręczne łączenie i pozwolić na pojedynczego providera na poziomie root.

    Wybierz między kontrolą eksplicytną a automatyzacją w zależności od preferencji zespołu.

    Użycie w komponencie klienckim

    Weźmy przykład komponentu klienckiego renderującego licznik.

    Tłumaczenia (po jednym pliku JSON na namespace w src/locales/...)

    src/locales/en/about.json
    Kopiuj kod

    Skopiuj kod do schowka

    {"title": "About","description": "About page description","counter": {  "label": "Counter",  "increment": "Increment"}}
    src/locales/fr/about.json
    Kopiuj kod

    Skopiuj kod do schowka

    {"title": "À propos","description": "Description de la page À propos","counter": {  "label": "Compteur",  "increment": "Incrémenter"}}

    Komponent kliencki (ładuje tylko wymagany namespace)

    src/components/ClientComponent.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    "use client";import React, { useState } from "react";import { useTranslation } from "react-i18next";const ClientComponent = () => {const { t, i18n } = useTranslation("about");const [count, setCount] = useState(0);const numberFormat = new Intl.NumberFormat(i18n.language);return (  <div>    <p>{numberFormat.format(count)}</p>    <button      aria-label={t("counter.label")}      onClick={() => setCount((c) => c + 1)}    >      {t("counter.increment")}    </button>  </div>);};export default ClientComponent;

    Upewnij się, że strona/dostawca zawiera tylko potrzebne przestrzenie nazw (np. about). Jeśli używasz React < 19, zapamiętuj ciężkie formatery, takie jak Intl.NumberFormat.

    Tłumaczenia (zachowano strukturę; załaduj je do wiadomości next-intl według własnego uznania)

    locales/en/about.json
    Kopiuj kod

    Skopiuj kod do schowka

    {"counter": {  "label": "Counter",  "increment": "Increment"}}
    locales/fr/about.json
    Kopiuj kod

    Skopiuj kod do schowka

    {"counter": {  "label": "Compteur",  "increment": "Incrémenter"}}

    Komponent klienta

    src/components/ClientComponentExample.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    "use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => {// Bezpośredni zakres do zagnieżdżonego obiektuconst t = useTranslations("about.counter");const format = useFormatter();const [count, setCount] = useState(0);return (  <div>    <p>{format.number(count)}</p>    <button      aria-label={t("label")}      onClick={() => setCount((count) => count + 1)}    >      {t("increment")}    </button>  </div>);};
    Nie zapomnij dodać komunikatu "about" na stronie klienta

    Zawartość

    src/components/ClientComponentExample/index.content.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import { t, type Dictionary } from "intlayer";const counterContent = {key: "counter",content: {  label: t({ pl: "Licznik", en: "Counter", fr: "Compteur" }),  increment: t({ pl: "Zwiększ", en: "Increment", fr: "Incrémenter" }),},} satisfies Dictionary;export default counterContent;

    Komponent klienta

    src/components/ClientComponentExample/index.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    "use client";import React, { useState } from "react";import { useNumber, useIntlayer } from "next-intlayer";const ClientComponentExample = () => {const [count, setCount] = useState(0);const { label, increment } = useIntlayer("counter"); // zwraca stringiconst { number } = useNumber();return (  <div>    <p>{number(count)}</p>    <button aria-label={label} onClick={() => setCount((count) => count + 1)}>      {increment}    </button>  </div>);};

    Porównanie

    • Formatowanie liczb

      • next-i18next: brak useNumber; używa Intl.NumberFormat (lub i18next-icu).
      • next-intl: useFormatter().number(value).
      • Intlayer: wbudowane useNumber().
    • Klucze

      • Zachowaj zagnieżdżoną strukturę (about.counter.label) i odpowiednio ustaw zakres hooka (useTranslation("about") + t("counter.label") lub useTranslations("about.counter") + t("label")).
    • Lokalizacje plików

      • next-i18next oczekuje plików JSON w public/locales/{lng}/{ns}.json.
      • next-intl jest elastyczny; ładuj wiadomości według własnej konfiguracji.
      • Intlayer przechowuje zawartość w słownikach TS/JS i rozwiązuje je za pomocą klucza.

    Użycie w komponencie serwerowym

    Weźmiemy pod uwagę przypadek komponentu UI. Ten komponent jest komponentem serwerowym i powinien mieć możliwość bycia wstawionym jako dziecko komponentu klienta. (strona (komponent serwerowy) -> komponent klienta -> komponent serwerowy). Ponieważ ten komponent może być wstawiony jako dziecko komponentu klienta, nie może być asynchroniczny.

    src/components/ServerComponent.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    type ServerComponentProps = {t: (key: string) => string;locale: string;count: number;};const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {const formatted = new Intl.NumberFormat(locale).format(count);return (  <div>    <p>{formatted}</p>    <button aria-label={t("counter.label")}>{t("counter.increment")}</button>  </div>);};export default ServerComponent;
    src/components/ServerComponent.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    type ServerComponentProps = {t: (key: string) => string;locale: string;count: number;formatter: Intl.NumberFormat;};const ServerComponent = ({t,locale,count,formatter,}: ServerComponentProps) => {const formatted = formatter.format(count);return (  <div>    <p>{formatted}</p>    <button aria-label={t("counter.label")}>{t("counter.increment")}</button>  </div>);};export default ServerComponent;

    Ponieważ komponent serwera nie może być asynchroniczny, musisz przekazać tłumaczenia i funkcję formatującą jako propsy.

    W twojej stronie / układzie:

    • import { getTranslations, getFormatter } from "next-intl/server";
    • const t = await getTranslations("about.counter");
    • const formatter = await getFormatter().then((formatter) => formatter.number());
    src/components/ServerComponent.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import { useIntlayer, useNumber } from "next-intlayer/server";type ServerComponentProps = {count: number;};const ServerComponent = ({ count }: ServerComponentProps) => {const { label, increment } = useIntlayer("counter");const { number } = useNumber();return (  <div>    <p>{number(count)}</p>    <button aria-label={label}>{increment}</button>  </div>);};
    Intlayer udostępnia bezpieczne dla serwera hooki za pośrednictwem next-intlayer/server. Aby działały, useIntlayer i useNumber używają składni podobnej do hooków klienta, ale pod spodem zależą od kontekstu serwera (IntlayerServerProvider).

    Metadane / Sitemap / Robots

    Tłumaczenie treści jest świetne. Jednak ludzie często zapominają, że głównym celem internacjonalizacji jest uczynienie Twojej strony bardziej widoczną dla świata. I18n to niesamowita dźwignia do poprawy widoczności Twojej strony.

    Oto lista dobrych praktyk dotyczących wielojęzycznego SEO.

    • ustaw metatagi hreflang w tagu <head> > Pomaga to wyszukiwarkom zrozumieć, jakie języki są dostępne na stronie
    • wymień wszystkie tłumaczenia stron w pliku sitemap.xml, używając schematu XML http://www.w3.org/1999/xhtml >
    • nie zapomnij wykluczyć stron z prefiksami w pliku robots.txt (np. /dashboard oraz /fr/dashboard, /es/dashboard) >
    • używaj niestandardowego komponentu Link, aby przekierować do najbardziej zlokalizowanej strony (np. po francusku <a href="/fr/about">A propos</a>) >

    Programiści często zapominają o prawidłowym referencjonowaniu swoich stron w różnych lokalizacjach.

    i18n.config.ts
    Kopiuj kod

    Skopiuj kod do schowka

    export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";export function abs(locale: string, path: string) { return ORIGIN + localizedPath(locale, path);}
    src/app/[locale]/about/layout.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";type GenerateMetadataParams = { params: Promise<{   locale: string; }>;};export const generateMetadata = async ({ params,}: GenerateMetadataParams): Promise<Metadata> => { const { locale } = await params; // Importuj odpowiedni pakiet JSON z src/locales const messages = (await import("@/locales/" + locale + "/about.json"))   .default; const languages = Object.fromEntries(   locales.map((locale) => [locale, localizedPath(locale, "/about")]) ); return {   title: messages.title,   description: messages.description,   alternates: {     canonical: localizedPath(locale, "/about"),     languages: { ...languages, "x-default": "/about" },   }, };};export default async function AboutPage() { return <h1>O nas</h1>;}
    src/app/sitemap.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import type { MetadataRoute } from "next";import { locales, defaultLocale, abs } from "@/i18n.config";export const sitemap = (): MetadataRoute.Sitemap => { const languages = Object.fromEntries(   locales.map((locale) => [locale, abs(locale, "/about")]) ); return [   {     url: abs(defaultLocale, "/about"),     lastModified: new Date(),     changeFrequency: "monthly",     priority: 0.7,     alternates: { languages },   }, ];};
    src/app/robots.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import type { MetadataRoute } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";const ORIGIN = "https://example.com";const expandAllLocales = (path: string) => [ localizedPath(defaultLocale, path), ...locales   .filter((locale) => locale !== defaultLocale)   .map((locale) => localizedPath(locale, path)),];export const robots = (): MetadataRoute.Robots => { const disallow = [   ...expandAllLocales("/dashboard"),   ...expandAllLocales("/admin"), ]; return {   rules: { userAgent: "*", allow: ["/"], disallow },   host: ORIGIN,   sitemap: ORIGIN + "/sitemap.xml", };};
    src/app/[locale]/about/layout.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";const localizedPath = (locale: string, path: string) => { // Zwraca ścieżkę lokalizowaną, jeśli locale jest domyślne, zwraca oryginalną ścieżkę return locale === defaultLocale ? path : "/" + locale + path;};type GenerateMetadataParams = { params: Promise<{   locale: string; }>;};export const generateMetadata = async ({ params,}: GenerateMetadataParams): Promise<Metadata> => { const { locale } = await params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries(   locales.map((locale) => [locale, localizedPath(locale, url)]) ); return {   title: t("title"),   description: t("description"),   alternates: {     canonical: localizedPath(locale, url),     languages: { ...languages, "x-default": url },   }, };};// ... Reszta kodu strony
    src/app/sitemap.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export const sitemap = (): MetadataRoute.Sitemap => { const aboutLanguages = Object.fromEntries(   locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [   {     url: formatterLocalizedPath(defaultLocale, "/about"),     lastModified: new Date(),     changeFrequency: "monthly", // częstotliwość zmian: miesięcznie     priority: 0.7, // priorytet strony     alternates: { languages: aboutLanguages }, // alternatywne wersje językowe   }, ];};
    src/app/robots.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales   .filter((locale) => locale !== defaultLocale)   .map((locale) => "/" + locale + path),];export const robots = (): MetadataRoute.Robots => { const disallow = [   ...withAllLocales("/dashboard"),   ...withAllLocales("/admin"), ]; return {   rules: { userAgent: "*", allow: ["/"], disallow },   host: origin,   sitemap: origin + "/sitemap.xml", };};
    src/app/[locale]/about/layout.tsx
    Kopiuj kod

    Skopiuj kod do schowka

    import { getIntlayer, getMultilingualUrls } from "intlayer";import type { Metadata } from "next";import type { LocalPromiseParams } from "next-intlayer";export const generateMetadata = async ({ params,}: LocalPromiseParams): Promise<Metadata> => { const { locale } = await params; const metadata = getIntlayer("page-metadata", locale); const multilingualUrls = getMultilingualUrls("/about"); return {   ...metadata,   alternates: {     canonical: multilingualUrls[locale as keyof typeof multilingualUrls],     languages: { ...multilingualUrls, "x-default": "/about" },   }, };};// ... Reszta kodu strony
    src/app/sitemap.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import { getMultilingualUrls } from "intlayer";import type { MetadataRoute } from "next";const sitemap = (): MetadataRoute.Sitemap => [ {   url: "https://example.com/about",   alternates: {     languages: { ...getMultilingualUrls("https://example.com/about") },   }, },];
    src/app/robots.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import { getMultilingualUrls } from "intlayer";import type { MetadataRoute } from "next";const getAllMultilingualUrls = (urls: string[]) => urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);const robots = (): MetadataRoute.Robots => ({ rules: {   userAgent: "*",   allow: ["/"],   disallow: getAllMultilingualUrls(["/dashboard"]), }, host: "https://example.com", sitemap: "https://example.com/sitemap.xml",});export default robots;
    Intlayer dostarcza funkcję getMultilingualUrls do generowania wielojęzycznych adresów URL dla Twojej mapy witryny.

    Middleware do routingu lokalizacji

    Dodaj middleware do obsługi wykrywania lokalizacji i routingu:

    src/middleware.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import { NextResponse, type NextRequest } from "next/server";import { defaultLocale, locales } from "@/i18n.config";const PUBLIC_FILE = /\.[^/]+$/; // wyklucz pliki z rozszerzeniamiexport function middleware(request: NextRequest) {const { pathname } = request.nextUrl;if (  pathname.startsWith("/_next") ||  pathname.startsWith("/api") ||  pathname.startsWith("/static") ||  PUBLIC_FILE.test(pathname)) {  return;}const hasLocale = locales.some(  (l) => pathname === "/" + l || pathname.startsWith("/" + l + "/"));if (!hasLocale) {  const locale = defaultLocale;  const url = request.nextUrl.clone();  url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);  return NextResponse.redirect(url);}}export const config = {matcher: [  // Dopasuj wszystkie ścieżki z wyjątkiem tych zaczynających się od podanych oraz plików z rozszerzeniem  "/((?!api|_next|static|.*\\..*).*)",],};

    Dodaj middleware do obsługi wykrywania i routingu lokalizacji:

    src/middleware.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import createMiddleware from "next-intl/middleware";import { locales, defaultLocale } from "@/i18n";export default createMiddleware({locales: [...locales],defaultLocale,localeDetection: true,});export const config = {// Pomijaj API, wewnętrzne elementy Next oraz zasoby statycznematcher: ["/((?!api|_next|.*\\..*).*)"],};

    Intlayer zapewnia wbudowane zarządzanie middleware za pomocą konfiguracji pakietu next-intlayer.

    src/middleware.ts
    Kopiuj kod

    Skopiuj kod do schowka

    import { intlayerProxy } from "next-intlayer/proxy";export const middleware = intlayerProxy();// stosuje ten middleware tylko do plików w katalogu appexport const config = {matcher: "/((?!api|_next|static|.*\\..*).*)",};

    Konfiguracja middleware jest scentralizowana w pliku intlayer.config.ts.

    Lista kontrolna konfiguracji i dobre praktyki

    • Upewnij się, że lang i dir są ustawione na głównym elemencie <html> w src/app/[locale]/layout.tsx.
    • Podziel tłumaczenia na przestrzenie nazw (na przykład common.json, about.json) w katalogu src/locales/<locale>/.
    • Ładuj tylko wymagane przestrzenie nazw w komponentach klienckich, używając useTranslation('<ns>') oraz ograniczając I18nProvider do tych samych przestrzeni nazw.
    • Utrzymuj strony statyczne, gdy to możliwe: eksportuj export const dynamic = 'force-static' na stronach; ustaw dynamicParams = false i zaimplementuj generateStaticParams.
    • Używaj synchronicznych komponentów serwerowych zagnieżdżonych pod granicami klienta, przekazując już obliczone ciągi znaków lub funkcję t oraz locale.
    • Dla SEO ustaw alternates.languages w metadanych, wymień lokalizowane adresy URL w sitemap.ts i zabroń duplikowania lokalizowanych tras w robots.ts.
    • Preferuj formatery uwzględniające lokalizację (np. Intl.NumberFormat(locale)) i zapamiętuj je po stronie klienta, jeśli używasz React < 19.
    • Ustaw atrybuty html lang i dir: W pliku src/app/[locale]/layout.tsx oblicz dir za pomocą getLocaleDirection(locale) i ustaw <html lang={locale} dir={dir}>.
    • Podziel wiadomości na przestrzenie nazw: Organizuj pliki JSON według lokalizacji i przestrzeni nazw (np. common.json, about.json).
    • Minimalizuj obciążenie klienta: Na stronach wysyłaj do NextIntlClientProvider tylko wymagane przestrzenie nazw (np. pick(messages, ['common', 'about'])).
    • Preferuj strony statyczne: Eksportuj export const dynamic = 'force-static' i generuj statyczne parametry dla wszystkich locales.
    • Synchroniczne komponenty serwerowe: Utrzymuj komponenty serwerowe synchroniczne, przekazując wcześniej obliczone ciągi znaków (przetłumaczone etykiety, sformatowane liczby) zamiast wywołań asynchronicznych lub funkcji nieserializowalnych.
    • Modularna zawartość: Umieszczaj słowniki zawartości razem z komponentami, korzystając z plików .content.{ts|js|json}.
    • Bezpieczeństwo typów: Wykorzystaj integrację z TypeScript do walidacji zawartości na etapie kompilacji.
    • Optymalizacja w czasie budowania: Używaj narzędzi Intlayer do automatycznego tree-shakingu i optymalizacji pakietu.
    • Zintegrowane narzędzia: Wykorzystaj wbudowane routowanie, pomocników SEO oraz wsparcie edytora wizualnego.

    A zwycięzca to…

    To nie jest proste. Każda opcja ma swoje kompromisy. Oto jak to widzę:

    next-i18next

    • dojrzały, pełen funkcji, wiele wtyczek społecznościowych, ale wyższy koszt konfiguracji. Jeśli potrzebujesz ekosystemu wtyczek i18next (np. zaawansowane reguły ICU przez wtyczki) i Twój zespół już zna i18next, akceptując więcej konfiguracji dla elastyczności.

    next-intl

    • najprostszy, lekki, mniej narzuconych decyzji. Jeśli chcesz minimalne rozwiązanie, czujesz się komfortowo z centralizowanymi katalogami, a Twoja aplikacja jest mała do średniej wielkości.

    Intlayer

    • zbudowany dla nowoczesnego Next.js, z modularną zawartością, bezpieczeństwem typów, narzędziami i mniejszą ilością boilerplate. Jeśli cenisz sobie zawartość ograniczoną do komponentu, ścisły TypeScript, gwarancje w czasie kompilacji, tree-shaking oraz wbudowane narzędzia do routingu/SEO/edytora – szczególnie dla Next.js App Router, systemów projektowych i dużych, modularnych baz kodu.

    Jeśli wolisz minimalną konfigurację i akceptujesz trochę ręcznego łączenia, next-intl jest dobrym wyborem. Jeśli potrzebujesz wszystkich funkcji i nie przeszkadza Ci złożoność, next-i18next sprawdzi się. Ale jeśli chcesz nowoczesne, skalowalne, modularne rozwiązanie z wbudowanymi narzędziami, Intlayer ma na celu dostarczyć to od razu po wyjęciu z pudełka.

    Alternatywa dla zespołów korporacyjnych: Jeśli potrzebujesz sprawdzonego rozwiązania, które działa doskonale z uznanymi platformami lokalizacyjnymi, takimi jak Crowdin, Phrase lub innymi profesjonalnymi systemami zarządzania tłumaczeniami, rozważ next-intl lub next-i18next ze względu na ich dojrzały ekosystem i sprawdzone integracje.
    Przyszła mapa drogowa: Intlayer planuje również rozwijać wtyczki działające na bazie rozwiązań i18next i next-intl. Dzięki temu zyskasz zalety Intlayer w zakresie automatyzacji, składni i zarządzania treścią, zachowując jednocześnie bezpieczeństwo i stabilność zapewnianą przez te sprawdzone rozwiązania w kodzie aplikacji.

    Gwiazdki GitHub (GitHub STARs)

    Gwiazdy na GitHubie są silnym wskaźnikiem popularności projektu, zaufania społeczności oraz jego długoterminowej istotności. Choć nie są bezpośrednią miarą jakości technicznej, odzwierciedlają, ilu deweloperów uważa projekt za przydatny, śledzi jego rozwój i prawdopodobnie zdecyduje się go używać. Przy szacowaniu wartości projektu, gwiazdy pomagają porównać zainteresowanie różnymi alternatywami oraz dostarczają wglądu w rozwój ekosystemu.

    Wykres historii gwiazd


    Podsumowanie

    Wszystkie trzy biblioteki odnoszą sukces w podstawowej lokalizacji. Różnica polega na tym, ile pracy musisz włożyć, aby osiągnąć solidną, skalowalną konfigurację w nowoczesnym Next.js:

    • W przypadku Intlayer modularna zawartość, ścisły TS, bezpieczeństwo w czasie kompilacji, tree-shaken bundles oraz pierwszorzędny App Router i narzędzia SEO są domyślne, a nie obowiązkowe.
    • Jeśli Twój zespół ceni łatwość utrzymania i szybkość w aplikacji wielojęzycznej, opartej na komponentach, Intlayer oferuje dziś najpełniejsze doświadczenie.

    Zapoznaj się z dokumentem 'Dlaczego Intlayer?' po więcej szczegółów.

    Automatyzacja vue-i18n
    react-i18next vs react-intl vs Intlayer
    Alt+→

    Na tej stronie

      Dyskusje są anonimowe i regularnie przeglądane w celu rozwiązania typowych problemów. Podziel się pomysłami na funkcje, opinią o dokumentacji lub czymkolwiek związanym z Intlayer, wykorzystujemy te informacje do kształtowania naszej mapy drogowej i ulepszania produktu.

      .├── i18n.config.ts└── src  ├── locales  │   ├── en  │   │  ├── common.json  │   │  └── about.json  │   └── fr  │      ├── common.json  │      └── about.json  ├── app  │   ├── i18n  │   │   └── server.ts  │   └── [locale]  │       ├── layout.tsx  │       └── about.tsx  └── components      ├── I18nProvider.tsx      ├── ClientComponent.tsx      └── ServerComponent.tsx
      .├── i18n.ts├── locales│   ├── en│   │  ├── home.json│   │  └── navbar.json│   ├── fr│   │  ├── home.json│   │  └── navbar.json│   └── es│      ├── home.json│      └── navbar.json└── src  ├── middleware.ts  ├── app  │   ├── i18n  │   │   └── server.ts  │   └── [locale]  │       └── home.tsx  └── components      └── Navbar          └── index.tsx
      .├── intlayer.config.ts└── src  ├── middleware.ts  ├── app  │   └── [locale]  │       ├── layout.tsx  │       └── home  │           ├── index.tsx  │           └── index.content.ts  └── components      └── Navbar          ├── index.tsx          └── index.content.ts
      export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export const rtlLocales = ["ar", "he", "fa", "ur"] as const;export const isRtl = (locale: string) =>(rtlLocales as readonly string[]).includes(locale);// Funkcja zwracająca ścieżkę z uwzględnieniem lokalizacjiexport function localizedPath(locale: string, path: string) {return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";// Funkcja zwracająca absolutny URL z lokalizacjąexport function abs(locale: string, path: string) {return ORIGIN + localizedPath(locale, path);}
      import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";// Ładowanie zasobów JSON z src/locales/<locale>/<namespace>.jsonconst backend = resourcesToBackend((locale: string, namespace: string) =>  import(`../../locales/${locale}/${namespace}.json`));export async function initI18next(locale: string,namespaces: string[] = ["common"]) {const i18n = createInstance();await i18n  .use(initReactI18next)  .use(backend)  .init({    lng: locale,    fallbackLng: defaultLocale,    ns: namespaces,    defaultNS: "common",    interpolation: { escapeValue: false },    react: { useSuspense: false },  });return i18n;}
      "use client";import * as React from "react";import { I18nextProvider } from "react-i18next";import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";const backend = resourcesToBackend((locale: string, namespace: string) =>  import(`../../locales/${locale}/${namespace}.json`));type Props = {locale: string;namespaces?: string[];resources?: Record<string, any>; // { ns: pakiet }children: React.ReactNode;};export default function I18nProvider({locale,namespaces = ["common"],resources,children,}: Props) {const [i18n] = React.useState(() => {  const i = createInstance();  i.use(initReactI18next)    .use(backend)    .init({      lng: locale,      fallbackLng: defaultLocale,      ns: namespaces,      resources: resources ? { [locale]: resources } : undefined,      defaultNS: "common",      interpolation: { escapeValue: false },      react: { useSuspense: false },    });  return i;});return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;}
      import type { ReactNode } from "react";import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";export const dynamicParams = false;export function generateStaticParams() {return locales.map((locale) => ({ locale }));}export default function LocaleLayout({children,params,}: {children: ReactNode;params: { locale: string };}) {const locale: Locale = (locales as readonly string[]).includes(params.locale)  ? params.locale  : defaultLocale;const dir = isRtl(locale) ? "rtl" : "ltr";return (  <html lang={locale} dir={dir}>    <body>{children}</body>  </html>);}
      import I18nProvider from "@/components/I18nProvider";import { initI18next } from "@/app/i18n/server";import type { Locale } from "@/i18n.config";import ClientComponent from "@/components/ClientComponent";import ServerComponent from "@/components/ServerComponent";// Wymuś statyczne renderowanie stronyexport const dynamic = "force-static";export default async function AboutPage({params: { locale },}: {params: { locale: Locale };}) {const namespaces = ["common", "about"] as const;const i18n = await initI18next(locale, [...namespaces]);const tAbout = i18n.getFixedT(locale, "about");return (  <I18nProvider locale={locale} namespaces={[...namespaces]}>    <main>      <h1>{tAbout("title")}</h1>      <ClientComponent />      <ServerComponent t={tAbout} locale={locale} count={0} />    </main>  </I18nProvider>);}
      import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";export const locales = ["en", "fr", "es"] as const;export const defaultLocale = "en" as const;async function loadMessages(locale: string) {// Załaduj tylko przestrzenie nazw potrzebne dla twojego layoutu/stronconst [common, about] = await Promise.all([  import(`../locales/${locale}/common.json`).then((m) => m.default),  import(`../locales/${locale}/about.json`).then((m) => m.default),]);return { common, about } as const;}export default getRequestConfig(async ({ locale }) => {if (!locales.includes(locale)) notFound();return {  messages: await loadMessages(locale),};});
      import type { ReactNode } from "react";import { locales } from "@/i18n";import {getLocaleDirection,unstable_setRequestLocale,} from "next-intl/server";export const dynamic = "force-static";export function generateStaticParams() {return locales.map((locale) => ({ locale }));}export default async function LocaleLayout({children,params,}: {children: ReactNode;params: Promise<{ locale: string }>;}) {const { locale } = await params;// Ustaw aktywny język żądania dla tego renderowania po stronie serwera (RSC)unstable_setRequestLocale(locale);const dir = getLocaleDirection(locale);return (  <html lang={locale} dir={dir}>    <body>{children}</body>  </html>);}
      import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponentExample from "@/components/ClientComponentExample";export const dynamic = "force-static";export default async function AboutPage({params,}: {params: Promise<{ locale: string }>;}) {const { locale } = await params;// Wiadomości są ładowane po stronie serwera. Przekaż tylko to, co jest potrzebne klientowi.const messages = await getMessages();const clientMessages = pick(messages, ["common", "about"]);// Tłumaczenia/formatowanie wyłącznie po stronie serweraconst tAbout = await getTranslations("about");const tCounter = await getTranslations("about.counter");const format = await getFormatter();const initialFormattedCount = format.number(0);return (  <NextIntlClientProvider locale={locale} messages={clientMessages}>    <main>      <h1>{tAbout("title")}</h1>      <ClientComponentExample />      <ServerComponent        formattedCount={initialFormattedCount}        label={tCounter("label")}        increment={tCounter("increment")}      />    </main>  </NextIntlClientProvider>);}
      import { type IntlayerConfig, Locales } from "intlayer";const config: IntlayerConfig = {internationalization: {  locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],  defaultLocale: Locales.ENGLISH,},};export default config;
      import { getHTMLTextDir } from "intlayer";import {IntlayerClientProvider,generateStaticParams,type NextLayoutIntlayer,} from "next-intlayer";export const dynamic = "force-static";const LocaleLayout: NextLayoutIntlayer = async ({ children, params }) => {const { locale } = await params;return (  <html lang={locale} dir={getHTMLTextDir(locale)}>    <body>      <IntlayerClientProvider locale={locale}>        {children}      </IntlayerClientProvider>    </body>  </html>);};export default LandingLayout;
      import { PageContent } from "@components/PageContent";import type { NextPageIntlayer } from "next-intlayer";import { IntlayerServerProvider, useIntlayer } from "next-intlayer/server";import { ClientComponent, ServerComponent } from "@components";const LandingPage: NextPageIntlayer = async ({ params }) => {const { locale } = await params;const { title } = useIntlayer("about", locale);return (  <IntlayerServerProvider locale={locale}>    <main>      <h1>{title}</h1>      <ClientComponent />      <ServerComponent />    </main>  </IntlayerServerProvider>);};export default LandingPage;
      {"title": "About","description": "About page description","counter": {  "label": "Counter",  "increment": "Increment"}}
      {"title": "À propos","description": "Description de la page À propos","counter": {  "label": "Compteur",  "increment": "Incrémenter"}}
      "use client";import React, { useState } from "react";import { useTranslation } from "react-i18next";const ClientComponent = () => {const { t, i18n } = useTranslation("about");const [count, setCount] = useState(0);const numberFormat = new Intl.NumberFormat(i18n.language);return (  <div>    <p>{numberFormat.format(count)}</p>    <button      aria-label={t("counter.label")}      onClick={() => setCount((c) => c + 1)}    >      {t("counter.increment")}    </button>  </div>);};export default ClientComponent;
      {"counter": {  "label": "Counter",  "increment": "Increment"}}
      {"counter": {  "label": "Compteur",  "increment": "Incrémenter"}}
      "use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => {// Bezpośredni zakres do zagnieżdżonego obiektuconst t = useTranslations("about.counter");const format = useFormatter();const [count, setCount] = useState(0);return (  <div>    <p>{format.number(count)}</p>    <button      aria-label={t("label")}      onClick={() => setCount((count) => count + 1)}    >      {t("increment")}    </button>  </div>);};
      import { t, type Dictionary } from "intlayer";const counterContent = {key: "counter",content: {  label: t({ pl: "Licznik", en: "Counter", fr: "Compteur" }),  increment: t({ pl: "Zwiększ", en: "Increment", fr: "Incrémenter" }),},} satisfies Dictionary;export default counterContent;
      "use client";import React, { useState } from "react";import { useNumber, useIntlayer } from "next-intlayer";const ClientComponentExample = () => {const [count, setCount] = useState(0);const { label, increment } = useIntlayer("counter"); // zwraca stringiconst { number } = useNumber();return (  <div>    <p>{number(count)}</p>    <button aria-label={label} onClick={() => setCount((count) => count + 1)}>      {increment}    </button>  </div>);};
      type ServerComponentProps = {t: (key: string) => string;locale: string;count: number;};const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {const formatted = new Intl.NumberFormat(locale).format(count);return (  <div>    <p>{formatted}</p>    <button aria-label={t("counter.label")}>{t("counter.increment")}</button>  </div>);};export default ServerComponent;
      type ServerComponentProps = {t: (key: string) => string;locale: string;count: number;formatter: Intl.NumberFormat;};const ServerComponent = ({t,locale,count,formatter,}: ServerComponentProps) => {const formatted = formatter.format(count);return (  <div>    <p>{formatted}</p>    <button aria-label={t("counter.label")}>{t("counter.increment")}</button>  </div>);};export default ServerComponent;
      import { useIntlayer, useNumber } from "next-intlayer/server";type ServerComponentProps = {count: number;};const ServerComponent = ({ count }: ServerComponentProps) => {const { label, increment } = useIntlayer("counter");const { number } = useNumber();return (  <div>    <p>{number(count)}</p>    <button aria-label={label}>{increment}</button>  </div>);};
      export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";export function abs(locale: string, path: string) { return ORIGIN + localizedPath(locale, path);}
      import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";type GenerateMetadataParams = { params: Promise<{   locale: string; }>;};export const generateMetadata = async ({ params,}: GenerateMetadataParams): Promise<Metadata> => { const { locale } = await params; // Importuj odpowiedni pakiet JSON z src/locales const messages = (await import("@/locales/" + locale + "/about.json"))   .default; const languages = Object.fromEntries(   locales.map((locale) => [locale, localizedPath(locale, "/about")]) ); return {   title: messages.title,   description: messages.description,   alternates: {     canonical: localizedPath(locale, "/about"),     languages: { ...languages, "x-default": "/about" },   }, };};export default async function AboutPage() { return <h1>O nas</h1>;}
      import type { MetadataRoute } from "next";import { locales, defaultLocale, abs } from "@/i18n.config";export const sitemap = (): MetadataRoute.Sitemap => { const languages = Object.fromEntries(   locales.map((locale) => [locale, abs(locale, "/about")]) ); return [   {     url: abs(defaultLocale, "/about"),     lastModified: new Date(),     changeFrequency: "monthly",     priority: 0.7,     alternates: { languages },   }, ];};
      import type { MetadataRoute } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";const ORIGIN = "https://example.com";const expandAllLocales = (path: string) => [ localizedPath(defaultLocale, path), ...locales   .filter((locale) => locale !== defaultLocale)   .map((locale) => localizedPath(locale, path)),];export const robots = (): MetadataRoute.Robots => { const disallow = [   ...expandAllLocales("/dashboard"),   ...expandAllLocales("/admin"), ]; return {   rules: { userAgent: "*", allow: ["/"], disallow },   host: ORIGIN,   sitemap: ORIGIN + "/sitemap.xml", };};
      import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";const localizedPath = (locale: string, path: string) => { // Zwraca ścieżkę lokalizowaną, jeśli locale jest domyślne, zwraca oryginalną ścieżkę return locale === defaultLocale ? path : "/" + locale + path;};type GenerateMetadataParams = { params: Promise<{   locale: string; }>;};export const generateMetadata = async ({ params,}: GenerateMetadataParams): Promise<Metadata> => { const { locale } = await params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries(   locales.map((locale) => [locale, localizedPath(locale, url)]) ); return {   title: t("title"),   description: t("description"),   alternates: {     canonical: localizedPath(locale, url),     languages: { ...languages, "x-default": url },   }, };};// ... Reszta kodu strony
      import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export const sitemap = (): MetadataRoute.Sitemap => { const aboutLanguages = Object.fromEntries(   locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [   {     url: formatterLocalizedPath(defaultLocale, "/about"),     lastModified: new Date(),     changeFrequency: "monthly", // częstotliwość zmian: miesięcznie     priority: 0.7, // priorytet strony     alternates: { languages: aboutLanguages }, // alternatywne wersje językowe   }, ];};
      import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales   .filter((locale) => locale !== defaultLocale)   .map((locale) => "/" + locale + path),];export const robots = (): MetadataRoute.Robots => { const disallow = [   ...withAllLocales("/dashboard"),   ...withAllLocales("/admin"), ]; return {   rules: { userAgent: "*", allow: ["/"], disallow },   host: origin,   sitemap: origin + "/sitemap.xml", };};
      import { getIntlayer, getMultilingualUrls } from "intlayer";import type { Metadata } from "next";import type { LocalPromiseParams } from "next-intlayer";export const generateMetadata = async ({ params,}: LocalPromiseParams): Promise<Metadata> => { const { locale } = await params; const metadata = getIntlayer("page-metadata", locale); const multilingualUrls = getMultilingualUrls("/about"); return {   ...metadata,   alternates: {     canonical: multilingualUrls[locale as keyof typeof multilingualUrls],     languages: { ...multilingualUrls, "x-default": "/about" },   }, };};// ... Reszta kodu strony
      import { getMultilingualUrls } from "intlayer";import type { MetadataRoute } from "next";const sitemap = (): MetadataRoute.Sitemap => [ {   url: "https://example.com/about",   alternates: {     languages: { ...getMultilingualUrls("https://example.com/about") },   }, },];
      import { getMultilingualUrls } from "intlayer";import type { MetadataRoute } from "next";const getAllMultilingualUrls = (urls: string[]) => urls.flatMap((url) => Object.values(getMultilingualUrls(url)) as string[]);const robots = (): MetadataRoute.Robots => ({ rules: {   userAgent: "*",   allow: ["/"],   disallow: getAllMultilingualUrls(["/dashboard"]), }, host: "https://example.com", sitemap: "https://example.com/sitemap.xml",});export default robots;
      import { NextResponse, type NextRequest } from "next/server";import { defaultLocale, locales } from "@/i18n.config";const PUBLIC_FILE = /\.[^/]+$/; // wyklucz pliki z rozszerzeniamiexport function middleware(request: NextRequest) {const { pathname } = request.nextUrl;if (  pathname.startsWith("/_next") ||  pathname.startsWith("/api") ||  pathname.startsWith("/static") ||  PUBLIC_FILE.test(pathname)) {  return;}const hasLocale = locales.some(  (l) => pathname === "/" + l || pathname.startsWith("/" + l + "/"));if (!hasLocale) {  const locale = defaultLocale;  const url = request.nextUrl.clone();  url.pathname = "/" + locale + (pathname === "/" ? "" : pathname);  return NextResponse.redirect(url);}}export const config = {matcher: [  // Dopasuj wszystkie ścieżki z wyjątkiem tych zaczynających się od podanych oraz plików z rozszerzeniem  "/((?!api|_next|static|.*\\..*).*)",],};
      import createMiddleware from "next-intl/middleware";import { locales, defaultLocale } from "@/i18n";export default createMiddleware({locales: [...locales],defaultLocale,localeDetection: true,});export const config = {// Pomijaj API, wewnętrzne elementy Next oraz zasoby statycznematcher: ["/((?!api|_next|.*\\..*).*)"],};
      import { intlayerProxy } from "next-intlayer/proxy";export const middleware = intlayerProxy();// stosuje ten middleware tylko do plików w katalogu appexport const config = {matcher: "/((?!api|_next|static|.*\\..*).*)",};