Автор:
    Создание: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-контент, то место, где большинство API становятся неудобными.
    6. Как долго проживет проект? Трехмесячный прототип и пятилетний продукт требуют совершенно разного объема build-инструментария.

    Запишите ответы. Всё изложенное ниже будет опираться на них.

    Общая картина

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

    История библиотек i18n в JavaScript

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

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

    Спроектированы с учетом SSR и Server Components. Рендеринг на сервере, гидратация только того, что требуется клиенту. По-прежнему централизованы и основаны на ключах.

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

    В статье об истории JavaScript i18n подробно описано, как каждая волна решала проблемы предыдущей.

    Главное решение: где живет контент и когда он загружается

    Каждая библиотека i18n для React устроена одинаково: store, provider, hook. Всё, что получает provider, оказывается в клиентском bundle или в данных гидратации. Поэтому существует два ключевых структурных выбора:

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

    На графике ниже показана расчетная нагрузка для теоретического приложения от 1 до 10 страниц, переведенного на 1–10 локалей, при объеме текста около 30 КБ на страницу.

    Теоретическая утечка контента в зависимости от архитектуры

    Централизованный контент со статическими импортами растет по обеим осям: 10 страниц, умноженные на 10 локалей, дают 300 КБ текста на каждой странице. Динамические импорты убирают зависимость от локалей. Локальное разделение (scoping) убирает зависимость от страниц. Только их сочетание позволяет графику оставаться плоским.

    Это свойство не самой библиотеки, а дисциплины разработки. react-i18next можно разделить с помощью namespaces и lazy backend. use-intl можно разбивать по маршрутам. Но ничто не заставляет это делать строго, и общий <Button>, вызывающий t("common:cta"), незаметно превращает common в зависимость для каждого маршрута. В бенчмарке это измеряется как "утечка из других маршрутов" и "утечка из других локалей", и именно здесь кроется большая часть разницы между библиотеками.

    Если вашим ответом на вопрос №3 было "много локалей, много страниц", уделите этому разделу больше внимания, чем любым предпочтениям по API. В статье о покомпонентном и централизованном i18n этот выбор рассматривается глубже с точки зрения поддержки кода.

    Кандидаты

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

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

    Две вещи, которые не отражены в таблице. Paraglide практически не добавляет размер библиотеки, поскольку генерирует код прямо в ваш репозиторий, что требует шага регенерации перед каждым коммитом и ведет к merge-конфликтам в сгенерированных файлах. А Intlayer требует плагин для bundler (vite-intlayer или аналог), поэтому его нельзя запустить в среде без сборки.

    Сопоставьте ваши ответы с библиотекой

    Выбирайте самый простой рабочий вариант и не усложняйте. react-i18next с одним JSON на локаль отлично подойдет, а десятилетний опыт ответов на Stack Overflow сэкономит вам время. Пропустите namespaces, пока они действительно не понадобятся. Если прототип перерастет в продукт, запланируйте миграцию на scoped-контент, адаптер совместимости react-i18next позволяет сделать это постепенно.

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

    Отдавайте предпочтение локальному контенту и динамической загрузке по умолчанию, а не по договоренности. Lingui и Paraglide достигают этого за счет компиляции. Intlayer реализует это через покомпонентные объявления, а компилятор включает в сборку только то, что рендерит маршрут. С react-i18next или use-intl планируйте стратегию namespaces и lazy-loading с первого дня и контролируйте ее на code review, так как инструменты этого делать не будут.

    Любая библиотека на основе ключей может быть типизирована, но почти ни одна не типизирована по умолчанию. Если вы не хотите поддерживать declaration merging, который должен корректно работать с лениво загружаемыми namespaces, выберите библиотеку, где типы генерируются из контента: Lingui, Paraglide или Intlayer. В статье об обнаружении недостающих переводов сравнивается, какие ошибки каждая из них находит на этапе сборки.

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

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

    React context не пересекает границу между сервером и клиентом. Библиотекам, построенным только на клиентском хуке (react-i18next, react-intl), потребуется параллельное серверное 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, а локаль считывается из storage на каждом узле вместо единого store.
    • Tolgee: нет типизации ключей, более сложный онбординг, главным преимуществом является редактирование in-context.
    • Intlayer: обязательный плагин для сборщика, меньшая экосистема, частичная поддержка ICU, контент распределен по codebase по концепции дизайна, поэтому для экспорта единого JSON для переводчика требуются специальные инструменты.
    • gt-react, lingo.dev: не рекомендованы по результатам бенчмарка: ошибки квот при сборке, привязка к вендору и проблемы с реактивностью, требовавшие принудительного повторного рендеринга provider.

    Как каждый вариант выглядит в коде

    Один и тот же компонент, сводка корзины с заголовком и плюрализацией, реализованный на каждом из кандидатов. Самое интересное здесь не сам компонент, а то, где находится контент и что о нем знает средство проверки типов.

    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-файлах с хешированными идентификаторами после выполнения lingui extract. Если забыть выполнить extract или compile, приложение без предупреждений переключится на английский fallback.

    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({
          ru: "Ваша корзина",
          en: "Your cart",
          fr: "Votre panier",
          es: "Tu carrito",
        }),
        items: t({
          ru: plural({
            one: "{{count}} товар",
            few: "{{count}} товара",
            many: "{{count}} товаров",
            other: "{{count}} товаров",
          }),
          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) создают псевдонимы для импортов на уровне bundler, благодаря чему существующий API продолжает работать, пока вы переносите проект компонент за компонентом. Остальные подробности описаны в руководстве по миграции.

    Перед тем как сделать выбор

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

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

    Коммиты, скорость ответов в issues и выходил ли последний минорный релиз в этом году. Продуманная архитектура без поддержки, это будущая вынужденная миграция.

    Не выбирайте исключительно по количеству скачиваний в npm.

    Самая скачиваемая библиотека, та, что появилась первой, а не та, которая лучше всего подходит для кодовой базы React в 2026 году. Количество загрузок отражает историю, а не применимость к вашим задачам.

    Рейтинг библиотек i18n для JavaScript

    Узнайте, кто финансирует поддержку и какие услуги продает.

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

    Готова ли библиотека к работе с AI-агентами?

    Агенты все еще испытывают сложности с i18n: они забывают локали, придумывают несуществующие ключи и путают синтаксисы сообщений. Предоставляет ли библиотека Agent Skills или MCP-сервер, чтобы агент мог просматривать, заполнять и тестировать контент? Оптимизирована ли загрузка контента по умолчанию, или кому-то придется каждый квартал проверять namespaces и lazy imports?

    Типобезопасность из коробки.

    Не «можно типизировать с помощью дополнительных настроек», а «неверный ключ приводит к ошибке tsc сразу после установки». Проверьте, что происходит при обращении к несуществующему ключу и если для какой-то локали пропущен перевод.

    Обнаружение неиспользуемого контента.

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

    Developer experience.

    Время от начала настройки до первой переведенной строки, наличие LSP или расширения для VS Code, показывающего перевод при наведении и позволяющего перейти к объявлению, CLI для заполнения, тестирования и отправки данных, а также возможность для не-разработчиков редактировать контент (визуальный редактор или CMS) без создания pull request.

    Часто задаваемые вопросы

    Да, для большинства команд. У нее самая крупная экосистема и больше всего готовых ответов в сети. Ее недостатки предсказуемы: самый тяжелый runtime, собственный формат плюрализации, а также необходимость самостоятельно настраивать и поддерживать типизацию и разделение на области видимости (scoping).

    Только если в число ваших требований входят минимальный размер bundle, сгенерированные типы или проверка отсутствующих ключей на этапе сборки. Для небольшого приложения с двумя локалями библиотеки с runtime-подходом будет достаточно. В статье о сравнении компиляторного и декларативного подходов в i18n подробно описаны преимущества компиляторов и возможные подводные камни.

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

    Косвенно. То, что видят поисковые роботы, определяется маршрутизацией, атрибутами hreflang, тегом <html lang> и наличием текста в HTML, отрендеренном на сервере. Некоторые библиотеки предоставляют хелперы для этого, большинство оставляют реализацию за вами. См. руководство по hreflang.

    Дополнительные материалы

    Комментарии

    Пока нет комментариев. Будьте первым, кто поделится своими мыслями.

    Похожие сообщения

    Последние сообщения