Автор:
    Создание:2026-09-13Последнее обновление:2026-09-13

    Lingui против Intlayer | Бенчмарк интернационализации (i18n) для React и Next.js

    Lingui и Intlayer - единственные две библиотеки в этом сравнительном исследовании, которые опираются на компилятор, а не на обычный рантайм. Lingui извлекает сообщения из макросов во время сборки и компилирует каталоги под каждую локаль. Intlayer компилирует словари на уровне компонентов и применяет tree-shaking отдельно для каждого языка. В теории их показатели должны быть близки. Цифры показывают, в чем они существенно расходятся.

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

    Кратко (tl;dr): Lingui ближе всех подобрался к Intlayer по объему чистого JavaScript на страницу: 115-120 КБ против 118,6 КБ на TanStack Start при настроенной ленивой загрузке, и 148,6 КБ против 141,3 КБ на Next.js. Однако в остальных аспектах образуется значительный разрыв: изолированно скомпилированный компонент Lingui весит 58-153 КБ против 6-8 КБ у Intlayer, гидратация занимает 28-34 мс против 11-14 мс, запасной текст исходной локали дает утечку 3-15% английских строк на французские страницы даже в оптимизированных сборках, а достижение такой оптимизации требует ручного извлечения, сборки и маршрутного разделения каталогов. Intlayer обеспечивает наилучшие показатели без какой-либо ручной настройки.

    Коротко о главном

    • Lingui - работает на макросах ( t`...` , <Trans>, msg), поддерживает синтаксис ICU MessageFormat, каталоги в форматах .po / JSON, пайплайн lingui extract + lingui compile. Компилирует идентификаторы в компактные хеши, поддерживает динамическую подгрузку каталогов по языкам. Зрелое, не привязанное к фреймворку решение с развитой инфраструктурой переводческих утилит вокруг формата .po.
    • Intlayer - компонентно-ориентированная модель контента. Словари .content.ts располагаются прямо рядом с целевым компонентом; компилятор на этапе сборки выполняет tree-shaking и загружает переводы по требованию для каждого компонента и языка; генерирует строгие типы TypeScript на основе контента, а пропущенные переводы вызывают ошибку сборки. Содержит встроенные middleware, SEO-хелперы, Визуальный редактор / CMS и модуль автоперевода через ИИ.
    БиблиотекаЗвезды на GitHubВсего коммитовПоследний коммитПервый релизВерсия в NPMЗагрузки в NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitАпрель 2024npmnpm downloads
    lingui/js-linguiGitHub Repo starsGitHub commit activityLast CommitДекабрь 2016npmnpm downloads
    Значки обновляются автоматически. Актуальные значения меняются со временем.

    Сопоставление функциональности

    ВозможностьIntlayer (react-intlayer / next-intlayer)Lingui (@lingui/core / @lingui/react)
    Переводы рядом с компонентами✅ Да, файл .content.ts хранится в одной папке с компонентом⚠️ Исходные строки внутри JSX через макросы; переводы в централизованных .po каталогах
    Интеграция с TypeScript✅ Строгая типизация автоматически генерируется из содержимого⚠️ Макросы типизированы; идентификаторы без типов, недостающие ключи не подсвечиваются
    Поиск отсутствующих переводов✅ Ошибка TypeScript + предупреждение/ошибка во время сборки⚠️ lingui extract показывает статистику; в рантайме тихо откатывается на английский текст
    Форматированный контент (JSX/Markdown/комп.)✅ Прямая встроенная поддержка✅ Компонент <Trans> со вложенными элементами
    Поддержка ICU⚠️ В разработке✅ Да (макросы plural, select, selectOrdinal)
    Форматирование (даты, числа, валюты)useNumber, useDate, ... (на базе нативного Intl)i18n.date(), i18n.number()
    Локализованный роутинг и middleware✅ Встроенный прокси/middleware, метод getMultilingualUrls❌ Отсутствует в базовом пакете
    Инструменты SEO (hreflang, sitemap...)✅ Встроенные хелперы❌ Требуется ручная реализация
    Синхронные серверные компоненты (RSC)useIntlayer из next-intlayer/server работает в любых дочерних серверных компонентах⚠️ Требуется экземпляр I18n на запрос, передаваемый вручную или через setI18n
    Tree-shaking (поставка только нужного)✅ На уровне компонентов и языков, автоматически компилятором⚠️ По языкам через lingui compile; по маршрутам требует ручного разделения файлов каталога
    Ленивая загрузка (Lazy loading)importMode: 'dynamic' (одна строка в конфигурационном файле)⚠️ Ручной вызов import() скомпилированных каталогов + i18n.load() / i18n.activate()
    Очистка неиспользуемого контента✅ Устаревшие словари удаляются во время сборкиlingui extract --clean удаляет неактуальные сообщения
    Проверка отсутствующих строк (CLI / CI)npx intlayer content test⚠️ Отчеты lingui extract (по умолчанию сборка не падает с ошибкой)
    Сборочный процесс✅ Один плагин (@intlayer/swc / @intlayer/babel / vite-intlayer)⚠️ Плагин макросов (Babel или SWC) + раздельные шаги extract и compile
    Перевод с помощью искусственного интеллекта✅ Встроен, использует ваши собственные ключи API❌ Нет
    Визуальный редактор / CMS✅ Бесплатный Visual Editor + подключаемая CMS❌ Нет (формат .po интегрируется со сторонними TMS)
    Сервер MCP и Agent Skills✅ Да❌ Нет
    Экосистема и сообщество⚠️ Моложе, но стремительно развивается✅ Зрелое, независимое от конкретного фреймворка решение

    Сравнительное тестирование

    Что исследовалось

    Пакет Benchmark Bloom собирает абсолютно идентичное приложение на каждой библиотеке: 10 страниц (home, about, blog, careers, contact, FAQ, pricing, products, settings, team), 10 локалей (en, fr, es, de, it, pt, zh, ja, ko, ru), одинаковая верстка и контент. Замеры проводились на страницах en и fr. Каждая библиотека исследовалась в четырех стратегиях загрузки:

    СтратегияОписаниеГде применяется
    staticКаталоги всех языков импортируются и загружаются целиком на стартеБыстрые прототипы, код от нейросетей
    dynamicЧерез import() подгружается каталог только активного языка, но для всего сайта сразуПодавляющее большинство проектов
    scoped-staticПо каталогу на каждый маршрут, все каталоги упакованы в общий стартовый бандлВстречается редко
    scoped-dynamicПо каталогу на маршрут + динамический import(). Только активная страница и нужный языкПроекты со строгим лимитом по весу

    У Intlayer отсутствует вариант "scoped": компилятор изолирует контент на уровне каждого компонента автоматически, поэтому режимы static и dynamic изначально работают максимально изолированно.

    В ходе каждого теста замеряются:

    • Lib size: вес gzip пустого компонента, подключающего исключительно библиотеку i18n (базовый оверхед рантайма).
    • Page JS: средний вес gzip JavaScript, загружаемого на страницу (усреднено по всем страницам и локалям).
    • Locale leak %: доля переведенных строк в загруженном JS, относящихся к языкам, которые пользователь не просматривает.
    • Page leak %: доля строк в загруженном JS, относящихся к страницам, на которых пользователь не находится.
    • Component avg: средний вес gzip отдельного компонента при изолированной сборке.
    • E2E reactivity: чистое время от момента выбора нового языка до фактического обновления атрибута html[lang] в DOM (Playwright, среднее по 5 прогонам).
    • Hydration: время завершения фазы гидратации React.
    Значения зафиксированы в тестировании от 2026-09-12 на базе @lingui/react 6.6.0 и intlayer 9.5.1. Тестовое приложение намеренно сделано компактным (несколько десятков фраз на язык), поэтому утечки наглядно отражают системную проблему: по мере роста проекта они пропорционально увеличиваются.

    Результаты на Next.js

    БиблиотекаСтратегияLib size (gz)Page JS ср. (gz)Утечка языкаУтечка страницКомпонент ср. (gz)E2E-реактивностьГидратация
    база (без i18n)-0,0 КБ141,0 КБ0,0%0,0%0,9 КБ13,4 мс11,8 мс
    Linguistatic11,9 КБ207,4 КБ50,0%90,0%73,3 КБ15,3 мс15,2 мс
    Linguidynamic11,9 КБ145,4 КБ2,8%89,9%19,9 КБ15,7 мс12,7 мс
    Linguiscoped-static11,9 КБ148,2 КБ2,7%89,1%20,4 КБ15,1 мс13,1 мс
    Linguiscoped-dynamic11,9 КБ148,6 КБ14,8%0,0%152,6 КБ16,1 мс14,8 мс
    next-intlayerstatic5,5 КБ141,3 КБ0,0%0,0%8,5 КБ15,5 мс16,9 мс
    next-intlayerdynamic5,5 КБ141,3 КБ0,0%0,0%6,9 КБ15,3 мс15,9 мс

    Анализ данных

    • Базовый вес рантайма. Пустой компонент с Lingui весит 11,9 КБ gzip против 5,5 КБ с Intlayer. В масштабе страницы лучшая сборка Lingui оказывается на +7,3 КБ тяжелее Intlayer (148,6 против 141,3 КБ); Intlayer добавляет всего +0,3 КБ к весу приложения без интернационализации.
    • Простая сборка создает колоссальный оверхед. Загрузка всех каталогов сразу дает 207,4 КБ на страницу (+66 КБ к базовому весу). Половина строк относится к чужому языку, а 90% - к посторонним страницам сайта.
    • Динамическая загрузка исправляет язык, но не маршруты. При наличии одного каталога на язык утечка страниц сохраняется на уровне ~90%: весь французский каталог отдается на каждой странице. Чтобы добиться 0% утечки страниц в Lingui, приходится настраивать scoped-dynamic: собирать отдельный каталог на каждый маршрут и вручную подключать его в страницы.
    • Утечка оригинального текста. Даже в наиболее продуманных схемах от 3% до 15% английских строк попадают на страницы на французском языке. Макросы Lingui встраивают исходную строку для подстраховки в итоговый бандл. Intlayer разрешает фолбеки на этапе сборки и доставляет клиенту только целевой язык.
    • Взрывной рост размера компонентов в scoped-dynamic. Каждый изолированный компонент весит в среднем 152,6 КБ, поскольку через цепочку импортов в него попадают каталоги других маршрутов. Тот же компонент на useIntlayer() занимает всего 6,9 КБ.

    Результаты на TanStack Start

    БиблиотекаСтратегияLib size (gz)Page JS ср. (gz)Утечка языкаУтечка страницКомпонент ср. (gz)E2E-реактивностьГидратация
    база (без i18n)-0,0 КБ111,0 КБ0,0%0,0%0,7 КБ8,1 мс21,6 мс
    Linguistatic11,2 КБ152,2 КБ50,0%90,0%58,0 КБ3,9 мс19,9 мс
    Linguidynamic11,2 КБ115,2 КБ9,3%0,0%85,5 КБ5,9 мс28,0 мс
    Linguiscoped-static11,2 КБ120,8 КБ4,0%0,0%147,9 КБ7,1 мс33,9 мс
    Linguiscoped-dynamic11,2 КБ120,2 КБ8,6%0,0%83,7 КБ42,1 мс32,9 мс
    intlayerstatic5,0 КБ125,8 КБ50,0%0,0%8,1 КБ3,2 мс11,5 мс
    intlayerdynamic5,0 КБ118,6 КБ0,0%0,0%6,3 КБ3,6 мс14,1 мс
    @intlayer/lingui (адаптер)dynamic10,3 КБ137,0 КБ9,9%0,0%12,8 КБ2,9 мс19,7 мс

    Анализ данных

    • По объему JS на страницу Lingui незначительно выигрывает. Вариант Lingui dynamic фиксирует 115,2 КБ, что на 3,4 КБ меньше, чем 118,6 КБ у Intlayer. Скомпилированные каталоги Lingui с хешированными ключами очень компактны, а роутер TanStack Start отлично разделяет код маршрутов, поэтому утечка страниц исчезает уже на шаге dynamic.
    • Все остальные метрики на стороне Intlayer. Гидратация с Lingui длится 28-34 мс против 11-14 мс с Intlayer: методы i18n.load() + i18n.activate() обязаны отработать на клиенте до старта гидратации React. Отдельные компоненты весят 58-148 КБ вместо 6-8 КБ. Утечка исходного языка не обнуляется никогда (составляя 4-9%).
    • Смена языка в оптимизированном режиме заметно замедлена. Lingui в схеме scoped-dynamic тратит 42 мс на смену html[lang], поскольку браузер должен сначала запросить, получить и активировать новый каталог маршрута. Intlayer переключает язык за 3-4 мс в обоих сценариях.
    • Строка static у Intlayer сразу обеспечивает 0% утечки страниц, так как в бандл включаются исключительно словари, импортированные задействованными на странице компонентами. А параметр importMode: 'dynamic' устраняет и языковую утечку.
    • @intlayer/lingui сохраняет синтаксис макросов Lingui, связывая их со словарями Intlayer. Это решение жертвует небольшим объемом на страницу (137 КБ из-за присутствия макро-рантайма), взамен давая облегченные компоненты (12,8 КБ) и быструю гидратацию. Прекрасный вариант для плавного перехода.

    В чем фундаментальная причина? Два компилятора, две единицы деления

    Обе библиотеки компилируют код, но принципиально отличается объект компиляции.

    Lingui компилирует каталоги. Макросы в коде извлекаются в единый файл .po на язык, а затем компилируются в модуль JS на язык. Главная единица деления - локаль. Чтобы разделить данные глубже (по маршрутам или компонентам), вам придется заводить отдельные каталоги, прописывать правила в lingui.config.ts и вручную настраивать их загрузку. Экземпляр I18n глобален, и каждый useLingui() подключает компонент ко всему его содержимому.

    bash
    .
    ├── lingui.config.ts
    └── src
        ├── i18n.ts                          # setupI18n(), load(), activate()
        ├── locales
       ├── en
       ├── messages.po
       └── messages.mjs             # вывод lingui compile
       └── fr
           ├── messages.po
           └── messages.mjs
        ├── components
       └── Counter.tsx                  # const { t } = useLingui(); t`Increment`
        └── routes
            └── $locale
                └── about.tsx                # await import(`../locales/${locale}/messages.mjs`)
    

    Intlayer компилирует словари. Каждый файл .content.ts - это изолированный словарь под определенным ключом; компилятор выясняет, какой компонент использует какой ключ, и формирует компактный JSON строго для этого компонента и языка. Главная единица деления - компонент. Привязка к маршрутам происходит автоматически: страница скачивает словари только тех компонентов, которые выводятся на экран.

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── components
       └── Counter
           ├── index.tsx                # useIntlayer("counter")
           └── index.content.ts
        └── routes
            └── $locale
                ├── about.tsx
                └── about.content.ts
    

    Именно поэтому архитектура scoped-dynamic формируется в Intlayer автоматически на этапе сборки, тогда как в Lingui это требует масштабной ручной настройки.

    Чтобы повторить показатели строки dynamic, активируйте dictionary.importMode: 'dynamic' в intlayer.config.ts. Подробнее в документации по оптимизации бандла.

    Опыт разработки (Developer Experience)

    Настройка

    Lingui

    lingui.config.ts
    import { defineConfig } from "@lingui/cli";
    
    export default defineConfig({
      sourceLocale: "en",
      locales: ["en", "fr"],
      catalogs: [
        {
          path: "<rootDir>/src/locales/{locale}/messages",
          include: ["src"],
        },
      ],
    });
    
    src/i18n.ts
    import { setupI18n } from "@lingui/core";
    
    export const loadCatalog = async (locale: string) => {
      const { messages } = await import(`./locales/${locale}/messages.mjs`);
      const i18n = setupI18n();
      i18n.load(locale, messages);
      i18n.activate(locale);
      return i18n;
    };
    

    Затем в сборщик добавляется плагин @lingui/babel-plugin-lingui-macro (или @lingui/swc-plugin), после правок выполняется lingui extract, перед сборкой - lingui compile, а приложение оборачивается в <I18nProvider i18n={i18n}>.

    Intlayer

    intlayer.config.ts
    import { type IntlayerConfig, Locales } from "intlayer";
    
    const config: IntlayerConfig = {
      internationalization: {
        locales: [Locales.ENGLISH, Locales.FRENCH],
        defaultLocale: Locales.ENGLISH,
      },
    };
    
    export default config;
    

    Добавьте плагин intlayer() в vite.config.ts (или withIntlayer() в next.config.ts) и оберните дерево компонентов в <IntlayerProvider>. Никаких ручных команд извлечения или сборки: словари компилируются автоматически бандлером.

    Компоненты

    Lingui

    src/components/Counter.tsx
    import { useState } from "react";
    import { useLingui } from "@lingui/react/macro";
    import { Trans } from "@lingui/react/macro";
    
    export const Counter = () => {
      const { t, i18n } = useLingui();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{i18n.number(count)}</p>
          <button aria-label={t`Counter`} onClick={() => setCount((c) => c + 1)}>
            <Trans>Increment</Trans>
          </button>
        </div>
      );
    };
    

    Английский текст пишется прямо в компоненте; перевод на французский хранится в src/locales/fr/messages.po под сгенерированным хеш-идентификатором после выполнения lingui extract. Если забыть запустить команду извлечения или сборки, на экране незаметно останется английский текст.

    Intlayer

    src/components/Counter/index.content.ts
    import { t, type Dictionary } from "intlayer";
    
    const counterContent = {
      key: "counter",
      content: {
        label: t({ en: "Counter", fr: "Compteur" }),
        increment: t({ en: "Increment", fr: "Incrémenter" }),
      },
    } satisfies Dictionary;
    
    export default counterContent;
    
    src/components/Counter/index.tsx
    import { useState } from "react";
    import { useIntlayer } from "react-intlayer";
    import { useNumber } from "react-intlayer/format";
    
    export const Counter = () => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label} onClick={() => setCount((c) => c + 1)}>
            {increment}
          </button>
        </div>
      );
    };
    

    Оба языка находятся в одном файле рядом с компонентом. Отсутствие перевода для fr приведет к ошибке сборщика, а опечатка в имени ключа будет немедленно подсвечена TypeScript.

    Вне компонентов React

    Метаданные, функции загрузки данных (loaders), серверные функции: в любом контексте без дерева React.

    Lingui

    src/routes/$locale/about.tsx
    import { setupI18n } from "@lingui/core";
    import { msg } from "@lingui/core/macro";
    
    const title = msg`About us`;
    
    export const loader = async ({ params }: { params: { locale: string } }) => {
      const { messages } = await import(
        `../../locales/${params.locale}/messages.mjs`
      );
      const i18n = setupI18n({
        locale: params.locale,
        messages: { [params.locale]: messages },
      });
    
      return { title: i18n._(title) };
    };
    

    Создание нового экземпляра I18n на каждый вызов, ручной импорт конкретного каталога и использование msg + i18n._() вместо привычного t. Как отмечается в примечаниях к бенчмарку, постоянный выбор между t, t` ` , i18n.t(), msg или <Trans> часто сбивает с толку.

    Intlayer

    src/routes/$locale/about.tsx
    import { getIntlayer } from "intlayer";
    
    export const loader = async ({ params }: { params: { locale: string } }) => {
      const { title } = getIntlayer("about-metadata", params.locale);
    
      return { title };
    };
    

    Использование макросов Lingui со словарями Intlayer

    @intlayer/lingui работает как прозрачный адаптер для @lingui/core и @lingui/react. Макросы компилируются в прежнем режиме; генерируемые вызовы i18n._() берут данные из словарей Intlayer, а плагины синхронизации сохраняют файлы .po в качестве источника данных. Формы множественного числа и условия ICU отображаются абсолютно идентично.

    vite.config.ts
    import { defineConfig } from "vite";
    import { lingui } from "@intlayer/lingui/plugin";
    
    export default defineConfig({
      plugins: [lingui()],
    });
    

    Оставьте @lingui/babel-plugin-lingui-macro или @lingui/swc-plugin в конфигурации сборщика перед компилятором Intlayer. См. документацию по совместимости с Lingui.

    Что выбрать для вашего проекта?

    • Выбирайте Lingui, если вам необходим синтаксис ICU MessageFormat с типизированными макросами, переводчики работают через файлы .po в сформированном процессе TMS, вам удобнее писать текст прямо в JSX, а команда готова поддерживать процесс извлечения, сборки и деления каталогов. При правильной настройке ленивой загрузки размер итоговой страницы крайне конкурентен.
    • Выбирайте Intlayer, если вам нужны изолированный контент на уровне компонентов, строгая типизация TypeScript, контроль отсутствующих ключей на этапе сборки, автоматический tree-shaking и ленивая загрузка без конфигурации, легковесные компоненты, мгновенная смена локалей и встроенные инструменты редактирования (Визуальный редактор, CMS, перевод через ИИ, сервер MCP). Идеально подходит для модульных систем и дизайн-систем.
    • Выбирайте @intlayer/lingui, если у вас уже есть кодовая база на Lingui и вы хотите постепенно перейти на архитектуру словарей Intlayer, не переписывая макросы.

    Похожие сравнения

    Звезды на GitHub

    Количество звезд на GitHub отражает интерес индустрии, уровень доверия сообщества и перспективность развития проекта. Это наглядно показывает, сколько разработчиков находят библиотеку полезной в реальной работе.

    График динамики звезд

    Заключение

    Lingui - сильнейшая гибридная библиотека в данном бенчмарке. Ее скомпилированные и хешированные каталоги дают размер JavaScript на страницу, практически равный показателям Intlayer, а на TanStack Start даже слегка превосходящий его. Если бы единственным критерием были килобайты на страницу, результатом стала бы боевая ничья.

    Но это не единственный показатель. Компилятор Lingui ограничен границами локали; все, что лежит ниже (каталоги маршрутов, ленивая загрузка, очистка запасного текста), требует ручного труда разработчика. Бенчмарк демонстрирует цену этого ограничения: компоненты в 10-20 раз тяжелее, гидратация в 2-3 раза медленнее, постоянная утечка 3-15% исходных текстов и пауза в 42 мс при переключении языка в оптимизированной сборке. Компилятор Intlayer работает на уровне каждого компонента, обеспечивая 6-8 КБ, 11-14 мс, 0% и 3-4 мс из коробки без лишней настройки.

    Все первичные данные, тестовые приложения и скрипты опубликованы в репозитории Benchmark Bloom. Вы можете повторить эти тесты самостоятельно.

    Подробнее читайте в материале 'Почему Intlayer?'.

    Комментарии

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

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

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