Autor:
    Data utworzenia:2026-09-16Ostatnia aktualizacja:2026-09-16

    Jak wybrać odpowiednią bibliotekę i18n dla React

    React nie dostarcza żadnego wbudowanego mechanizmu i18n. Biblioteka, którą wybierzesz na samym początku, decyduje o tym, jak przechowywane są tłumaczenia, jak trafiają do bundle i jak duża część pracy spadnie na Ciebie przez kolejne lata. Większość zespołów wybiera rozwiązanie na podstawie popularności, a kompromisy odkrywa dopiero przy 2 000 kluczy.

    Ten przewodnik podchodzi do tematu od drugiej strony: najpierw odpowiedz na kilka pytań dotyczących Twojego projektu, a następnie dopasuj odpowiedzi do pasujących bibliotek. Koncentruje się na czystym React (Vite, React Router, TanStack Start). Next.js ma własne ograniczenia, opisane w porównaniu Next.js.

    Ekosystem bibliotek React i18n

    Spis treści

    Sześć pytań, na które warto odpowiedzieć przed porównaniem bibliotek

    Tabela funkcji jest bezużyteczna, jeśli nie wiesz, które wiersze mają dla Ciebie znaczenie. Przejdź najpierw przez poniższe punkty.

    1. Jak renderowana jest aplikacja? Tylko SPA, SSR z hydratacją czy React Server Components. Hooki oparte na kontekście działają wszędzie w SPA. W przypadku RSC hook wymusza "use client" na każdym komponencie renderującym tekst, więc będziesz potrzebować również API po stronie serwera.
    2. Kto tworzy tłumaczenia? Programiści, wewnętrzny zespół korzystający z TMS, agencja dostarczająca pliki ICU czy pipeline AI. To determinuje format katalogu bardziej niż jakikolwiek szczegół API.
    3. Ile masz języków (locales) i podstron? Dwie wersje językowe i pięć stron mogą pozwolić sobie na wysłanie wszystkiego do klienta. Dziesięć wersji językowych i pięćdziesiąt tras już nie, a strategia ładowania staje się głównym kosztem.
    4. Czy potrzebujesz typowania kluczy? Literówka w t("checkout.totl") skompiluje się w każdej bibliotece opartej na kluczach, chyba że samodzielnie skonfigurujesz typy. Zdecyduj, czy to jest akceptowalne.
    5. Co zawiera string? Zwykły tekst, liczbę mnogą czy zdania z komponentem <Link> w środku. Bogata treść (rich content) to miejsce, w którym większość API staje się niewygodna.
    6. Jak długo będzie żył projekt? Trzymiesięczny prototyp i pięcioletni produkt nie potrzebują takiej samej ilości konfiguracji narzędzi budowania.

    Zapisz swoje odpowiedzi. Wszystko poniżej odnosi się właśnie do nich.

    Krajobraz w jednym ujęciu

    Piętnaście lat JavaScript i18n mieści się w czterech falach architektonicznych, a biblioteki React, które będziesz porównywać, pochodzą z różnych etapów.

    Historia bibliotek JavaScript i18n

    Katalogi JSON ładowane do pamięci, t("a.b") wyszukiwane w runtime, ICU lub niestandardowa składnia parsowana w przeglądarce. Największe ekosystemy, najcięższe runtime, typowanie jest opcjonalne.

    Wiadomości wyodrębniane podczas budowania, kompilowane do kompaktowych katalogów, typowane argumenty. Dodatkowy krok budowania (extract, compile) w zamian za mniejsze paczki bundle.

    Zaprojektowane z myślą o SSR i Server Components. Renderowanie na serwerze, hydratacja tylko tego, czego klient potrzebuje. Wciąż oparte na kluczach i scentralizowane.

    Treść jest kompilowana do funkcji zdatnych do tree-shakingu lub słowników per-komponent. Typy są generowane automatycznie, brakujące tłumaczenia powodują błąd kompilacji, a tłumaczenie AI działa bezpośrednio z poziomu CLI.

    Artykuł historia JavaScript i18n szczegółowo opisuje, jak każda fala odpowiadała na problemy poprzedniej.

    Najważniejsza decyzja: gdzie żyje treść i kiedy jest ładowana

    Każda biblioteka React i18n ma podobny schemat: store, provider i hook. Cokolwiek otrzymuje provider, ląduje w bundle klienta lub w payloadzie hydratacji. Dwa kluczowe wybory strukturalne to:

    • Treść scentralizowana czy modułowa (scoped). Jeden plik en.json dla całej aplikacji, czy jedna deklaracja na komponent (lub na przestrzeń nazw).
    • Import statyczny czy dynamiczny. Wszystko spakowane przy starcie, czy aktywny język i trasa pobierane na żądanie.

    Poniższy wykres szacuje rozmiar danych dla teoretycznej aplikacji mającej od 1 do 10 stron, przetłumaczonej na 1 do 10 języków, z około 30 KB tekstu na stronę.

    Teoretyczny wyciek treści według architektury

    Scentralizowana treść ze statycznymi importami rośnie wzdłuż obu osi: 10 stron razy 10 języków to 300 KB tekstu na każdej stronie. Dynamiczne importy eliminują oś języków. Modułowość (scoping) eliminuje oś stron. Tylko połączenie obu podejść pozwala zachować stały, niski rozmiar.

    Nie jest to cecha samej biblioteki, lecz kwestia dyscypliny w projekcie. react-i18next może być podzielony na przestrzenie nazw z asynchronicznymi backendami. use-intl można podzielić per trasa. Jednak nic tego nie wymusza, a współdzielony <Button> sięgający po t("common:cta") po cichu czyni common zależnością każdej trasy. W benchmarku jest to mierzone jako "wyciek z innych tras" i "wyciek z innych języków" i to stąd wynika większość różnic między bibliotekami.

    Jeśli Twoją odpowiedzią na pytanie 3 było "wiele języków, wiele stron", nadaj tej sekcji większą wagę niż jakimkolwiek preferencjom API. Wpis per-component vs centralized i18n zagłębia się w kwestię utrzymania tego samego wyboru.

    Kandydaci

    Rozmiary bibliotek pochodzą z benchmarku TanStack Start: provider oraz hook w pustym komponencie, po bundlowaniu, tree-shakingu i minifikacji, 10 stron i 10 języków. Treść jest mierzona osobno.

    BibliotekaFalaModel treściTypowanie kluczyFormat wiadomościRozmiar biblioteki
    react-i18nextRuntimeCentralny JSON, przestrzenie nazwOpcjonalne (CustomTypeOptions)i18next (przyrostki dla liczb mnogich)~18.4 kB
    react-intl (FormatJS)RuntimeCentralny JSON, ICUOpcjonalne (ekstrakcja + unia)ICU~15.3 kB
    use-intlServer-firstCentralny JSON, ICUOpcjonalne (declaration merging)ICU~14.1 kB
    @tolgee/reactRuntimeCentralny, edycja w kontekścieBrakICU~11.1 kB
    LinguiMacroTekst źródłowy w kodzie, skompilowane katalogiDobre, z kompilatoraICU przez makraMały
    ParaglideCompilerProjekt inlang, wygenerowane funkcjeGenerowaneWłasnyBliski zeru
    IntlayerCompiler.content.ts per komponentGenerowane, domyślnie włączonePomocniki (plural, enu)Linia bazowa
    Liczby są zrzutem stanu w wersjach z benchmarku i zmieniają się wraz z kolejnymi wydaniami. Przed podjęciem decyzji wyłącznie na podstawie rozmiaru uruchom benchmark we własnej aplikacji.

    Dwie rzeczy, których tabela nie pokazuje: Paraglide nie dodaje niemal żadnej biblioteki runtime, ponieważ generuje kod bezpośrednio w Twoim repozytorium, co oznacza krok regeneracji przed każdym commitem i potencjalne konflikty scalania w wygenerowanych plikach. Z kolei Intlayer wymaga wtyczki do bundlera (vite-intlayer lub odpowiednika), więc nie może działać w środowisku bez etapu budowania.

    Dopasuj swoje odpowiedzi do biblioteki

    Wybierz najprostszą działającą opcję i nie komplikuj konfiguracji. react-i18next z pojedynczym plikiem JSON per język sprawdzi się doskonale, a dekada odpowiedzi na Stack Overflow zaoszczędzi Twój czas. Pomiń przestrzenie nazw, dopóki nie będą potrzebne. Jeśli prototyp przekształci się w produkt, zaplanuj migrację do treści modułowej; adapter kompatybilności react-i18next compat adapter pozwala przeprowadzić ją stopniowo.

    Twój format katalogu został już narzucony z góry. react-intl natywnie obsługuje ICU, a narzędzia ekstrakcji FormatJS są zbudowane pod taki proces. use-intl również odczytuje ICU. react-i18next w przeciwnym razie wymaga wtyczki ICU i własnych kluczy liczb mnogich. Obsługa ICU w Intlayer jest wciąż częściowa, więc jeśli już dziś otrzymujesz ciągi ICU, potraktuj to jako kwestię blokującą do czasu pełnej implementacji.

    Wybierz modułową treść i dynamiczne ładowanie jako standard domyślny, a nie tylko konwencję. Lingui oraz Paraglide osiągają to poprzez kompilację. Intlayer osiąga to dzięki deklaracjom per-komponent, a kompilator dostarcza tylko to, co renderuje dana trasa. W przypadku react-i18next lub use-intl zaplanuj strategię przestrzeni nazw i leniwego ładowania już pierwszego dnia i egzekwuj ją podczas code review, ponieważ narzędzia tego nie wymuszą.

    Każdą bibliotekę opartą na kluczach można otypować, ale prawie żadna nie oferuje tego od razu po instalacji. Jeśli nie chcesz utrzymywać mechanizmu declaration merging, który musi obsługiwać leniwie ładowane przestrzenie nazw, wybierz bibliotekę, w której typy są generowane z treści: Lingui, Paraglide lub Intlayer. Artykuł wykrywanie brakujących tłumaczeń porównuje, co każde rozwiązanie wyłapuje w czasie budowania.

    Rozbudowane węzły to miejsce, w którym podejście t() zwracające string przestaje się sprawdzać. react-i18next i Lingui mają <Trans>, react-intl posiada tagi rich text, a wszystkie te rozwiązania są bardziej kłopotliwe niż prosty przypadek tekstowy. Węzły treści Intlayer przyjmują bezpośrednio JSX, markdown oraz zagnieżdżone obiekty, co sprawdza się znacznie lepiej, gdy treść to coś więcej niż tylko etykiety interfejsu.

    Wtedy scentralizowany JSON przestaje być wymogiem, ponieważ nie ma potrzeby importu do zewnętrznego TMS. Treść umieszczona przy komponentach wraz z CLI uzupełniającym brakujące języki to krótsza droga. Polecenie fill w Intlayer działa z Twoim własnym kluczem API (OpenAI, Anthropic, Mistral, Gemini) i tłumaczy wyłącznie to, co uległo zmianie. Paraglide i Tolgee oferują hostowane odpowiedniki z własnymi planami taryfowymi.

    Kontekst React nie przekracza granicy między serwerem a klientem. Biblioteki zbudowane wyłącznie na hooku klienckim (react-i18next, react-intl) będą wymagały równoległego API serwerowego w dniu wdrożenia RSC. use-intl (jako next-intl) oraz Intlayer (jako next-intlayer) mają już ten podział wbudowany. Przeczytaj artykuł o Next.js i18n przed standaryzacją wzorca.

    Gdzie każda biblioteka ma swoje ograniczenia

    Szczere podsumowanie słabszych stron, ponieważ każda opcja ma swoje kompromisy.

    • react-i18next: najcięższa w zestawieniu, własny format liczb mnogich, typy wymagają ręcznej konfiguracji i utrzymania, nieużywane klucze gromadzą się po cichu.
    • react-intl: rozwlekłe DX (useIntl(), a następnie formatMessage({ id })), globalna instancja powiązana z wieloma węzłami.
    • use-intl: prosta na start, uciążliwa w optymalizacji. Przestrzenie nazw, dynamiczne ładowanie i typy użyte razem znacząco spowalniają development.
    • Lingui: dodatkowy krok budowania extract / compile, kilka nakładających się składni (t(), tagged template, i18n.t(), <Trans>), które dezorientują zarówno ludzi, jak i asystentów AI.
    • Paraglide: wygenerowane pliki w repozytorium, tree-shaking nie zadziałał w benchmarku React, a język jest odczytywany ze storage przy każdym węźle zamiast ze store'a.
    • Tolgee: brak typowania kluczy, trudniejszy onboarding, głównym atutem jest edycja w kontekście aplikacji.
    • Intlayer: wymagana wtyczka do budowania, mniejszy ekosystem, częściowe wsparcie dla ICU, treść rozproszona w całym codebase z założenia, więc wyeksportowanie jednego pliku JSON dla tłumacza wymaga dedykowanych narzędzi.
    • gt-react, lingo.dev: niezalecane w benchmarku ze względu na błędy limitów przy budowaniu, uzależnienie od dostawcy (vendor lock-in) i problemy z reaktywnością wymagające wymuszania ponownego renderowania providera.

    Jak każda opcja wygląda w kodzie

    Ten sam komponent, podsumowanie koszyka z tytułem i liczbą mnogą, napisany przy użyciu każdego z kandydatów. Najciekawszym elementem nie jest sam komponent, lecz miejsce przechowywania treści i to, co wie o niej system sprawdzania typów.

    public/locales/en/cart.json
    {
      "title": "Your cart",
      "items_one": "{{count}} item",
      "items_other": "{{count}} items"
    }
    
    src/components/CartSummary.tsx
    import type { FC } from "react";
    import { useTranslation } from "react-i18next";
    
    export const CartSummary: FC<{ count: number }> = ({ count }) => {
      const { t } = useTranslation("cart");
    
      return (
        <section>
          <h2>{t("title")}</h2>
          <p>{t("items", { count })}</p>
        </section>
      );
    };
    

    Liczby mnogie to klucze z przyrostkami rozwiązywane przez Intl.PluralRules. t ma typ (key: string) => string, chyba że zadeklarujesz CustomTypeOptions, więc t("titel") skompiluje się bez błędu.

    src/locales/en.json
    {
      "cart.title": "Your cart",
      "cart.items": "{count, plural, one {# item} other {# items}}"
    }
    
    src/components/CartSummary.tsx
    import type { FC } from "react";
    import { FormattedMessage, useIntl } from "react-intl";
    
    export const CartSummary: FC<{ count: number }> = ({ count }) => {
      const intl = useIntl();
    
      return (
        <section>
          <h2>
            <FormattedMessage id="cart.title" />
          </h2>
          <p>{intl.formatMessage({ id: "cart.items" }, { count })}</p>
        </section>
      );
    };
    

    ICU od początku do końca, czyli format najczęściej eksportowany przez platformy TMS. Typy dla id wynikają z kroku ekstrakcji formatjs oraz wygenerowanej unii, nie są dostępne od razu po instalacji.

    messages/en.json
    {
      "Cart": {
        "title": "Your cart",
        "items": "{count, plural, one {# item} other {# items}}"
      }
    }
    
    src/components/CartSummary.tsx
    import type { FC } from "react";
    import { useTranslations } from "use-intl";
    
    export const CartSummary: FC<{ count: number }> = ({ count }) => {
      const t = useTranslations("Cart");
    
      return (
        <section>
          <h2>{t("title")}</h2>
          <p>{t("items", { count })}</p>
        </section>
      );
    };
    

    Taka sama struktura jak w next-intl bez powiązań z Next.js. Klucze są typowane po rozszerzeniu AppConfig o typ wiadomości; podział na przestrzenie nazw leży po Twojej stronie.

    src/locales/fr/messages.po
    msgid "Your cart"
    msgstr "Votre panier"
    
    msgid "{count, plural, one {# item} other {# items}}"
    msgstr "{count, plural, one {# article} other {# articles}}"
    
    src/components/CartSummary.tsx
    import type { FC } from "react";
    import { Plural, Trans } from "@lingui/react/macro";
    
    export const CartSummary: FC<{ count: number }> = ({ count }) => (
      <section>
        <h2>
          <Trans>Your cart</Trans>
        </h2>
        <p>
          <Plural value={count} one="# item" other="# items" />
        </p>
      </section>
    );
    

    Język źródłowy znajduje się w komponencie; inne języki trafiają do plików .po pod skróconymi identyfikatorami po wykonaniu lingui extract. Pominięcie extract lub compile powoduje cichy powrót do języka angielskiego.

    messages/en.json
    {
      "cart_title": "Your cart",
      "cart_items": "{count} items"
    }
    
    src/components/CartSummary.tsx
    import type { FC } from "react";
    import { m } from "../paraglide/messages.js";
    
    export const CartSummary: FC<{ count: number }> = ({ count }) => (
      <section>
        <h2>{m.cart_title()}</h2>
        <p>{m.cart_items({ count })}</p>
      </section>
    );
    

    Każda wiadomość to wygenerowana, typowana funkcja, więc brakujący klucz jest błędem importu. Folder paraglide/ jest generowany w Twoim repozytorium i odświeżany przy każdej zmianie.

    src/components/cartSummary.content.ts
    import { plural, t, type Dictionary } from "intlayer";
    
    const cartSummaryContent = {
      key: "cart-summary",
      content: {
        title: t({
          pl: "Twój koszyk",
          en: "Your cart",
          fr: "Votre panier",
          es: "Tu carrito",
        }),
        items: t({
          pl: plural({ one: "{{count}} element", other: "{{count}} elementów" }),
          en: plural({ one: "{{count}} item", other: "{{count}} items" }),
          fr: plural({ one: "{{count}} article", other: "{{count}} articles" }),
          es: plural({ one: "{{count}} artículo", other: "{{count}} artículos" }),
        }),
      },
    } satisfies Dictionary;
    
    export default cartSummaryContent;
    
    src/components/CartSummary.tsx
    import type { FC } from "react";
    import { useIntlayer } from "react-intlayer";
    
    export const CartSummary: FC<{ count: number }> = ({ count }) => {
      const { title, items } = useIntlayer("cart-summary");
    
      return (
        <section>
          <h2>{title}</h2>
          <p>{items(count)}</p>
        </section>
      );
    };
    

    Wszystkie języki w jednym pliku obok komponentu. Typy są generowane podczas budowania, więc title podpowiada się w autouzupełnianiu, a literówka wywoła błąd tsc bez konieczności konfiguracji declaration merging. Usunięcie folderu usuwa powiązane stringi.

    Korzystasz już z react-i18next, react-intl lub Lingui? Adaptery kompatybilności (react-i18next, react-intl, Lingui) tworzą aliasy importów na poziomie bundlera, dzięki czemu dotychczasowe API nadal działa, podczas gdy Ty migrujesz kod komponent po komponencie. Resztę opisuje przewodnik migracji.

    Zanim podejmiesz ostateczną decyzję

    Tabela funkcji pokazuje, co biblioteka potrafi dzisiaj. Poniższe punkty pokazują, jak będzie wyglądać codzienna praca z nią.

    Sprawdź aktywność repozytorium.

    Liczba commitów, czas odpowiedzi na zgłoszenia i to, czy ostatnie wydanie minor pojawiło się w tym roku. Dobry projekt bez aktywnego opiekuna to przyszła migracja.

    Nie wybieraj wyłącznie na podstawie pobrań z npm.

    Najczęściej instalowana biblioteka to ta, która powstała jako pierwsza, a nie ta, która najlepiej pasuje do bazy kodu React w 2026 roku. Liczba pobrań mierzy historię, a nie dopasowanie do bieżących potrzeb.

    Ranking bibliotek JavaScript i18n

    Sprawdź, kto finansuje maintainera i co sprzedaje.

    i18next jest wspierany przez Locize. next-intl / use-intl, vue-i18n, svelte-i18n i Lingui są wspierane przez Crowdin. Tolgee, Paraglide (inlang) i Intlayer prowadzą własne platformy. Dostawca, którego przychód opiera się na hostowanym tłumaczeniu, ma niewielki interes w tym, aby tłumaczenie wewnątrz Twojego łańcucha narzędzi było darmowe. Intlayer jako jedyny z tego zestawu oferuje tłumaczenie AI przez CLI z Twoim własnym kluczem API oraz CMS, który możesz hostować samodzielnie.

    Czy rozwiązanie jest gotowe na agentów AI?

    Agenci wciąż miewają trudności z i18n: zapominają o wersjach językowych, wymyślają klucze i mieszają składnie wiadomości. Czy biblioteka dostarcza Agent Skills lub serwer MCP, aby agent mógł listować, uzupełniać i testować treść? Oraz czy ładowanie treści jest domyślnie zoptymalizowane, czy ktoś musi co kwartał weryfikować przestrzenie nazw i leniwe importy?

    Bezpieczeństwo typów od razu po instalacji.

    Nie "możliwe do otypowania po dodatkowej konfiguracji", ale "błędny klucz wywołuje błąd tsc w nowo zainstalowanym projekcie". Sprawdź, co dzieje się z kluczem, który nie istnieje, oraz z językiem, w którym brakuje jednego tłumaczenia.

    Wykrywanie nieużywanej treści.

    Katalogi z czasem tylko rosną. Proces budowania w Intlayer usuwa nieużywane pola i rejestruje je w logach (build.purge). Paraglide osiąga to dzięki architekturze, ponieważ niewywołana funkcja wiadomości jest usuwana w procesie tree-shakingu. Wszystkie pozostałe narzędzia pozostawiają czyszczenie po Twojej stronie.

    Developer Experience.

    Czas od konfiguracji do pierwszego przetłumaczonego ciągu znaków, LSP lub rozszerzenie VS Code pokazujące tłumaczenie po najechaniu kursorem i przenoszące do deklaracji, CLI do uzupełniania (fill), testowania i synchronizacji (push), a także możliwość edycji treści przez osoby nietechniczne (edytor wizualny lub CMS) bez konieczności tworzenia pull requesta.

    Najczęściej zadawane pytania (FAQ)

    Tak, dla większości zespołów. Posiada największy ekosystem i najwięcej materiałów w sieci. Koszty są realne, ale przewidywalne: najcięższy runtime, niestandardowy format liczb mnogich oraz bezpieczeństwo typów i podział na moduły, które musisz samodzielnie skonfigurować i utrzymać.

    Tylko wtedy, gdy rozmiar bundle, wygenerowane typy lub sprawdzanie brakujących kluczy w czasie budowania należą do Twoich kluczowych wymagań. W przypadku małej aplikacji z dwoma językami biblioteka działająca w czasie wykonywania jest prostsza. Artykuł kompilator kontra deklaratywne i18n wyjaśnia, jakie korzyści dają kompilatory i jakie mogą stwarzać wyzwania.

    Częściowo. Biblioteki oparte na kluczach dzielą na tyle podobną strukturę, że adapter kompatybilności może zmapować jedno API na drugie, co właśnie robią adaptery Intlayer. Formaty wiadomości (ICU vs i18next vs funkcje pomocnicze) nie konwertują się automatycznie, więc liczby mnogie oraz interpolacja będą elementami wymagającymi modyfikacji.

    Pośrednio. To, co widzą roboty wyszukiwarek, zależy od routingu, hreflang, atrybutu <html lang> oraz od tego, czy tekst znajduje się w kodzie HTML renderowanym na serwerze. Niektóre biblioteki dostarczają do tego pomocniki, większość pozostawia to programiście. Zobacz przewodnik po hreflang.

    Więcej informacji

    Komentarze

    Nie ma jeszcze komentarzy. Bądź pierwszą osobą, która podzieli się swoimi przemyśleniami.

    Powiązane posty

    Ostatnie posty