Автор:
    Дата створення:2026-09-16Останнє оновлення:2026-09-16

    Як вибрати правильну бібліотеку i18n для React

    React не має вбудованих примітивів для i18n. Бібліотека, яку ви оберете в перший день, визначає, як зберігатимуться переклади, як вони потраплять у bundle і скільки роботи залишиться за вами на найближчі кілька років. Більшість команд обирають за популярністю, а потім стикаються з компромісами, коли база перекладів досягає 2 000 ключів.

    Цей посібник пропонує піти від зворотного: спочатку дайте відповідь на кілька запитань про ваш проєкт, а потім зіставте відповіді з бібліотеками, які вам підходять. Він орієнтований на чистий React (Vite, React Router, TanStack Start). Next.js має власні обмеження, розглянуті у порівнянні Next.js.

    Екосистема бібліотек i18n для React

    Table of Contents

    Шість запитань перед порівнянням бібліотек

    Таблиця можливостей марна без розуміння того, які саме пункти важливі для вас. Спочатку пройдіться по цих запитаннях.

    1. Як рендериться застосунок? Лише SPA, SSR з гідратацією або React Server Components. Хуки на основі Context працюють скрізь у SPA. З RSC хук змушує додавати "use client" до кожного компонента, який рендерить текст, тому вам також знадобиться server-side API.
    2. Хто пише переклади? Розробники, внутрішня команда через TMS, агенція, що надає файли ICU, або AI-пайплайн. Це диктує формат каталогу набагато більше, ніж будь-які деталі API.
    3. Скільки локалей і сторінок? Дві локалі та п'ять сторінок можуть дозволити собі надсилати все одразу. Десять локалей і п'ятдесят маршрутів не можуть, і стратегія завантаження стає головною статтею витрат.
    4. Чи потрібна типізація ключів? Друкарська помилка в t("checkout.totl") скомпілюється в будь-якій бібліотеці на основі ключів, якщо ви не налаштуєте типи власноруч. Вирішіть, чи це прийнятно.
    5. Що містить рядок? Звичайний текст, множину або речення з <Link> посередині. Rich content, це те місце, де більшість API стають незручними.
    6. Як довго житиме проєкт? Тримісячний прототип і п'ятирічний продукт не потребують однакового обсягу build tooling.

    Запишіть відповіді. Усе викладене нижче посилатиметься на них.

    Загальна картина

    П'ятнадцять років JavaScript i18n укладаються в чотири архітектурні хвилі, і бібліотеки React, які ви порівнюватимете, належать до різних з них.

    Історія бібліотек i18n у JavaScript

    JSON-каталоги, завантажені в пам'ять, пошук t("a.b") під час виконання, ICU або кастомний синтаксис, що парситься у браузері. Найбільші екосистеми, найважчий runtime, типи підключаються опціонально.

    Повідомлення витягуються під час збірки, компілюються в компактні каталоги, типізовані аргументи. Додатковий крок збірки (extract, compile) в обмін на менший bundle.

    Спроєктовано навколо SSR та Server Components. Рендеринг на сервері, гідратація лише того, що потрібно клієнту. Усе ще на основі ключів та централізовано.

    Контент компілюється в tree-shakable функції або словники для окремих компонентів. Типи генеруються автоматично, відсутні переклади призводять до помилки збірки, а AI-переклад запускається з CLI.

    Історія JavaScript i18n детально описує, як кожна хвиля вирішувала проблеми попередньої.

    Найважливіше рішення: де живе контент і коли він завантажується

    Кожна бібліотека React i18n має схожу структуру: сховище, провайдер, хук. Усе, що отримує провайдер, потрапляє у клієнтський bundle або в payload гідратації. Отже, є два ключові структурні вибори:

    • Централізований або scoped контент. Один en.json для всього застосунку або одна декларація на компонент (чи на namespace).
    • Статичний або динамічний імпорт. Усе запаковано в bundle на старті, або активна локаль і маршрут завантажуються на вимогу.

    Графік нижче оцінює payload для теоретичного застосунку від 1 до 10 сторінок, перекладеного на 1–10 локалей, приблизно по 30 KB тексту на сторінку.

    Теоретичний витік контенту за архітектурою

    Централізований контент зі статичними імпортами зростає за обома осями: 10 сторінок, помножені на 10 локалей, дають 300 KB тексту на кожній сторінці. Динамічні імпорти прибирають вісь локалей. Scoping прибирає вісь сторінок. Лише їхня комбінація залишає графік плоским.

    Це не стільки властивість бібліотеки, скільки питання дисципліни розробки. react-i18next можна розділити за допомогою namespaces та lazy backends. use-intl можна розбити по маршрутах. Але ніщо не змушує цього робити, і спільний <Button>, який викликає t("common:cta"), непомітно робить common залежністю кожного маршруту. Бенчмарк вимірює це як "витік з інших маршрутів" та "витік з інших локалей", і саме тут виникає більша частина розриву між бібліотеками.

    Якщо вашою відповіддю на запитання 3 було "багато локалей, багато сторінок", надайте цьому розділу більшої ваги, ніж будь-яким уподобанням щодо API. Стаття per-component проти централізованого i18n детальніше розглядає сторону підтримки того самого вибору.

    Кандидати

    Розміри бібліотек взяті з бенчмарку TanStack Start: провайдер плюс хук у порожньому компоненті після збірки, tree-shaking та мініфікації, для 10 сторінок і 10 локалей. Контент вимірюється окремо.

    БібліотекаХвиляМодель контентуТипи для ключівФормат повідомленьРозмір бібліотеки
    react-i18nextRuntimeЦентральний JSON, namespacesOpt-in (CustomTypeOptions)i18next (суфікси множини)~18.4 kB
    react-intl (FormatJS)RuntimeЦентральний JSON, ICUOpt-in (витяг + union)ICU~15.3 kB
    use-intlServer-firstЦентральний JSON, ICUOpt-in (declaration merging)ICU~14.1 kB
    @tolgee/reactRuntimeЦентральний, in-context редагуванняНіICU~11.1 kB
    LinguiMacroВихідний текст у коді, скомпільовані каталогиДобре, від компілятораICU через макросиНевеликий
    ParaglideCompilerПроєкт inlang, згенеровані функціїЗгенерованіВласнийМайже нульовий
    IntlayerCompiler.content.ts для кожного компонентаЗгенеровані, увімкнені за замовчуваннямХелпери (plural, enu)Базовий
    Числа є знімком версій на момент бенчмарку і змінюються з новими релізами. Запустіть бенчмарк на власному застосунку, перш ніж приймати рішення лише на основі розміру.

    Дві речі, яких немає в таблиці. Paraglide майже не постачає бібліотечного коду, оскільки генерує код безпосередньо у ваш репозиторій, що означає крок регенерації перед кожним комітом і конфлікти злиття у згенерованих файлах. А Intlayer вимагає плагіна для бандлера (vite-intlayer або аналог), тому не може працювати в середовищі без етапу збірки (no-build setup).

    Зіставте свої відповіді з бібліотекою

    Обирайте найпростіший робочий варіант і не перевантажуйте архітектуру. react-i18next з одним файлом JSON на локаль чудово підійде, а десятирічний досвід відповідей на Stack Overflow заощадить ваш час. Відкладіть namespaces, поки вони дійсно не знадобляться. Якщо прототип переросте в продукт, закладіть час на міграцію до scoped контенту; адаптер сумісності react-i18next зробить цей процес поступовим.

    Формат каталогу вже вирішено за вас. react-intl нативно підтримує ICU, а інструменти вилучення FormatJS створені саме під цей пайплайн. use-intl також читає ICU. Для react-i18next знадобиться плагін ICU, інакше доведеться використовувати власні ключі множини. Підтримка ICU в Intlayer наразі часткова, тому якщо ви вже отримуєте рядки в ICU, вважайте це блокуючим фактором, поки функціонал не буде повністю реалізовано.

    Віддавайте перевагу scoped контенту та динамічному завантаженню за замовчуванням, а не за домовленістю. Lingui та Paraglide досягають цього завдяки компіляції. Intlayer досягає цього за допомогою оголошень для окремих компонентів, а компілятор додає до збірки лише те, що рендерить конкретний маршрут. З react-i18next або use-intl плануйте стратегію namespaces та lazy loading з першого дня і контролюйте її під час code review, оскільки інструменти цього не зроблять автоматично.

    Будь-яку бібліотеку на основі ключів можна типізувати, але майже жодна не робить цього за замовчуванням. Якщо ви не хочете підтримувати declaration merging, який має працювати з динамічно завантажуваними namespaces, оберіть бібліотеку, де типи генеруються з контенту: Lingui, Paraglide або Intlayer. Стаття про виявлення відсутніх перекладів порівнює, що саме кожна бібліотека відловлює під час збірки.

    Вузли rich content, це місце, де підхід з t(), що повертає звичайний рядок, починає давати збої. react-i18next і Lingui мають компонент <Trans>, react-intl пропонує теги rich text, і все це виглядає менш зручно, ніж робота зі звичайним текстом. Вузли контенту Intlayer приймають JSX, markdown та вкладені об'єкти безпосередньо, що значно зручніше, якщо контент містить щось складніше за прості текстові мітки інтерфейсу.

    Тоді централізований JSON більше не є обов'язковою вимогою, оскільки немає потреби імпортувати дані в зовнішню TMS. Колокований контент разом із CLI, який заповнює відсутні локалі, стає значно коротшим шляхом. Команда fill в Intlayer працює з вашим власним API ключем (OpenAI, Anthropic, Mistral, Gemini) і перекладає лише те, що змінилося. Paraglide та Tolgee пропонують хостингові аналоги зі своїми тарифними планами.

    React Context не перетинає межу між сервером і клієнтом. Бібліотеки, побудовані лише на клієнтських хуках (react-i18next, react-intl), потребуватимуть паралельного server API у той день, коли ви перейдете на RSC. use-intl (як next-intl) та Intlayer (як next-intlayer) уже мають такий розподіл. Ознайомтеся зі статтею про i18n у Next.js, перш ніж стандартизувати архітектурний патерн.

    Слабкі сторони кожної бібліотеки

    Чесні обмеження, оскільки кожен варіант має свої мінуси.

    • react-i18next: найважча серед усіх, власний формат множини, типи вимагають ручного налаштування та підтримки, невикористані ключі накопичуються непомітно.
    • react-intl: громіздкий DX (useIntl(), потім formatMessage({ id })), глобальний екземпляр прив'язаний до багатьох вузлів.
    • use-intl: просто розпочати, складно оптимізувати. Namespaces, динамічне завантаження та типи разом суттєво уповільнюють розробку.
    • Lingui: додатковий крок збірки extract / compile, кілька синтаксисів, що перетинаються (t(), tagged template, i18n.t(), <Trans>), які заплутують як людей, так і AI-асистентів.
    • Paraglide: згенеровані файли в репозиторії, tree-shaking не спрацював у бенчмарку React, а локаль зчитується зі сховища для кожного вузла замість централізованого store.
    • Tolgee: немає типізації ключів, складніший онбординг, головна перевага, редагування in-context.
    • Intlayer: обов'язковий плагін для збірки, менша екосистема, часткова підтримка ICU, контент за задумом розподілений по кодовій базі, тому для експорту єдиного JSON перекладачу потрібні додаткові інструменти.
    • gt-react, lingo.dev: не рекомендовані в бенчмарку: помилки квот під час збірки, vendor lock-in та проблеми з реактивністю, що вимагали примусового перерендеру провайдера.

    Як кожен варіант виглядає в коді

    Один і той самий компонент, підсумок кошика із заголовком та множиною, реалізований за допомогою кожного кандидата. Найцікавіше, не сам компонент, а те, де розташований контент і що про нього знає перевірка типів.

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

    Множини є суфіксними ключами, що обробляються через Intl.PluralRules. t має тип (key: string) => string, якщо не оголошено CustomTypeOptions, тому t("titel") скомпілюється без помилок.

    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, тобто формат, який експортує більшість платформ TMS. Типи для id з'являються завдяки кроку витягування formatjs та згенерованому union-типу, а не з коробки.

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

    Така ж структура, як у next-intl, але без прив'язок до Next.js. Ключі типізуються після розширення AppConfig типом повідомлень; розділення namespaces залишається за вами.

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

    Вихідна мова розміщується в компоненті; інші локалі зберігаються у файлах .po під хешованими id після виклику lingui extract. Якщо забути виконати extract або compile, застосунок тихо повертатиме англійську версію за замовчуванням.

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

    Кожне повідомлення є згенерованою типізованою функцією, тому відсутній ключ спричиняє помилку імпорту. Папка paraglide/ генерується у вашому репозиторії та оновлюється при кожній зміні.

    src/components/cartSummary.content.ts
    import { plural, t, type Dictionary } from "intlayer";
    
    const cartSummaryContent = {
      key: "cart-summary",
      content: {
        title: t({ en: "Your cart", fr: "Votre panier", es: "Tu carrito" }),
        items: t({
          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>
      );
    };
    

    Усі локалі в одному файлі поруч із компонентом. Типи генеруються під час збірки, тому для title працює автодоповнення, а друкарська помилка призводить до збою tsc без необхідності у declaration merging. Видалення папки призводить до видалення відповідних рядків.

    Вже використовуєте react-i18next, react-intl або Lingui? Адаптери сумісності (react-i18next, react-intl, Lingui) створюють аліаси для імпортів на рівні бандлера, тому наявне API продовжує працювати, поки ви переносите проєкт компонент за компонентом. Посібник з міграції описує решту кроків.

    Перед тим як зробити вибір

    Таблиця функцій показує, що вміє бібліотека сьогодні. Наведені нижче пункти допоможуть зрозуміти, як це буде працювати на практиці у довгостроковій перспективі.

    Перевірте активність репозиторію.

    Коміти, час відповіді на issue та те, чи виходив останній мінорний реліз цього року. Чудова архітектура без супроводу авторів, це потенційна міграція в майбутньому.

    Не обирайте за кількістю завантажень в npm.

    Найбільш встановлювана бібліотека, це та, що з'явилася першою, а не та, яка найкраще підходить для кодової бази React у 2026 році. Кількість завантажень відображає історію, а не відповідність потребам.

    Рейтинг бібліотек i18n у JavaScript

    Дізнайтеся, хто фінансує підтримку та що саме вони продають.

    i18next підтримується Locize. next-intl / use-intl, vue-i18n, svelte-i18n та Lingui підтримуються Crowdin. Tolgee, Paraglide (inlang) та Intlayer мають власні платформи. Вендор, чий дохід залежить від хостингу перекладів, навряд чи робитиме процес перекладу повністю безкоштовним всередині вашого інструментарію. Intlayer, єдине рішення серед перелічених, що надає AI-переклад через CLI з вашим власним API ключем та CMS, яку можна хостити самостійно.

    Чи готове рішення до роботи з AI-агентами?

    Агенти все ще відчувають труднощі з i18n: вони забувають локалі, вигадують ключі та змішують синтаксис повідомлень. Чи надає бібліотека Agent Skills або сервер MCP, щоб агент міг переглядати, заповнювати та тестувати контент? І чи оптимізовано завантаження контенту за замовчуванням, чи комусь доведеться щокварталу перевіряти namespaces та lazy imports?

    Типобезпека з коробки.

    Не "можна типізувати з додатковими налаштуваннями", а "неправильний ключ викликає помилку tsc на щойно встановленому проєкті". Перевірте, що відбувається з неіснуючим ключем і з локаллю, у якій бракує одного перекладу.

    Виявлення невикористаного контенту.

    Каталоги лише розростаються. Збірка Intlayer видаляє невикористані поля та логує їх (build.purge). Paraglide досягає цього завдяки архітектурі, оскільки невикликана функція повідомлення видаляється через tree-shaking. Всі інші рішення залишають очищення кодової бази на вас.

    Досвід розробника (DX).

    Час від налаштування до першого перекладеного рядка, LSP або розширення VS Code, яке показує переклад при наведенні курсора та переходить до декларації, CLI для заповнення, тестування й публікації, а також можливість редагування контенту для не-розробників (візуальний редактор або CMS) без відкриття pull request.

    Поширені запитання

    Так, для більшості команд. Вона має найбільшу екосистему та найбільшу кількість відповідей в інтернеті. Її мінуси відчутні, але передбачувані: найважчий runtime, власний формат множин, а також типобезпека зі scoping, які вам доведеться налаштовувати та контролювати самостійно.

    Лише якщо розмір bundle, згенеровані типи або перевірка відсутніх ключів під час збірки є серед ваших обов'язкових вимог. Для невеликого застосунку з двома локалями бібліотека з runtime-підходом буде простішою. Стаття компілятор проти декларативного i18n пояснює, які переваги надають компілятори і які нюанси вони можуть мати.

    Частково. Бібліотеки на основі ключів мають достатньо схожу структуру, щоб адаптер сумісності міг створити аліас одного API для іншого, саме так працюють адаптери Intlayer. Формати повідомлень (ICU проти i18next проти хелперів) не конвертуються автоматично, тому множина та інтерполяція будуть тими частинами, які доведеться змінити вручну.

    Опосередковано. Те, що бачать пошукові роботи, визначається маршрутизацією, тегами hreflang, <html lang> і тим, чи присутній текст у серверному HTML. Деякі бібліотеки мають готові хелпери для цього, більшість залишає це на вас. Дивіться посібник з hreflang.

    Поглиблені матеріали

    Коментарі

    Поки що немає коментарів. Будьте першим, хто поділиться своїми думками.

    Схожі публікації

    Останні публікації