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

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

    Lingui та Intlayer - це дві бібліотеки в цьому бенчмарку, які покладаються на компілятор, а не на чистий runtime. Lingui витягує повідомлення з макросів під час збирання та компілює каталоги для кожної локалі. Intlayer компілює словники для кожного компонента та виконує tree-shaking для кожної локалі. У теорії вони мають бути близькими. Числа показують, де вони розходяться.

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

    Коротко (tl;dr): Lingui найближче підходить до Intlayer за обсягом чистого JavaScript на сторінку: 115-120 КБ проти 118.6 КБ на TanStack Start після налаштування лінивого завантаження (lazy loading), 148.6 КБ проти 141.3 КБ на Next.js. Розрив виникає в інших показниках: компонент Lingui, скомпільований ізольовано, важить 58-153 КБ проти 6-8 КБ у Intlayer, гідратація займає 28-34 мс проти 11-14 мс, резервна локаль джерела витікає на 3-15% рядків en на сторінках fr у кожній оптимізованій конфігурації, а досягнення цієї оптимізованої конфігурації вимагає ручного вилучення, компіляції та вибору каталогів для кожного маршруту. 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, robots)✅ Вбудовані інструменти❌ Вручну
    Синхронні серверні компоненти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 (за замовчуванням немає коду помилки завершення)
    Конвеєр збирання (Build pipeline)✅ Один плагін (@intlayer/swc / @intlayer/babel / vite-intlayer)⚠️ Плагін макросів (Babel або SWC) + кроки extract + compile
    Переклад за допомогою ШІ✅ Вбудовано, використовує ваші власні ключі постачальників❌ Немає
    Візуальний редактор / CMS✅ Безкоштовний Візуальний редактор + додаткова 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. Постійні витрати runtime.
    • JS сторінки (Page JS): стиснутий gzip JavaScript, що завантажується на сторінку, усереднений для всіх сторінок і локалей.
    • % витоку локалі (Locale leak %): частка перекладених рядків у завантаженому JS, які належать локалі, яку користувач не переглядає (протестовано на en і fr, тому 50% означає повну присутність іншої вимірюваної мови; при 10 запакованих локалях реальні зайві витрати набагато більші).
    • % витоку сторінки (Page leak %): частка перекладених рядків у завантаженому JS, які належать сторінці, на якій користувач не перебуває.
    • Середній розмір компонента (Component avg): середній розмір gzip кожного компонента, скомпільованого окремо. Показує, скільки runtime та каталогів тягне за собою один компонент.
    • Реактивність E2E: час між вибором нової локалі та оновленням html[lang] у DOM (Playwright, 5 ітерацій).
    • Гідратація: тривалість фази гідратації React.
    Наведені нижче дані отримані під час тестування від 2026-09-12 з @lingui/react 6.6.0 та intlayer 9.5.1. Тестовий додаток навмисно невеликий (кілька десятків рядків на локаль), тому відсотки витоку описують закономірність: вони збільшуються разом зі зростанням вашого контенту, тоді як витрати runtime залишаються фіксованими.

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

    БібліотекаСтратегіяРозмір Lib (gz)Сер. JS сторінки (gz)Витік локаліВитік сторінкиСер. компонента (gz)Реактивність E2EГідратація
    base (без i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    Linguistatic11.9 KB207.4 KB50.0%90.0%73.3 KB15.3 ms15.2 ms
    Linguidynamic11.9 KB145.4 KB2.8%89.9%19.9 KB15.7 ms12.7 ms
    Linguiscoped-static11.9 KB148.2 KB2.7%89.1%20.4 KB15.1 ms13.1 ms
    Linguiscoped-dynamic11.9 KB148.6 KB14.8%0.0%152.6 KB16.1 ms14.8 ms
    next-intlayerstatic5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayerdynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms

    Як інтерпретувати результати

    • Витрати runtime. Порожній компонент важить 11.9 КБ gzip з Lingui і 5.5 КБ з Intlayer. На повній сторінці найкраща конфігурація Lingui дає +7.3 КБ у порівнянні з Intlayer (148.6 проти 141.3 КБ); Intlayer додає лише +0.3 КБ до базового додатка без i18n.
    • Проста конфігурація коштує дорого. Попереднє завантаження всіх скомпільованих каталогів дає 207.4 КБ на сторінку, що на +66 КБ більше за базовий додаток. Половина виявлених рядків належить до іншої локалі, а 90% - до інших сторінок.
    • Динамічне завантаження вирішує проблему локалі, але не сторінки. З одним каталогом на локаль витік сторінки залишається на рівні ~90%: весь каталог fr завантажується на кожній французькій сторінці. Щоб досягти 0% витоку сторінки, потрібна конфігурація scoped-dynamic: окремий каталог для кожного маршруту, який створюється та підключається вручну.
    • Витік резервної мови (fallback). Навіть в оптимізованих конфігураціях 3-15% рядків en потрапляють на сторінки fr. Макроси Lingui зберігають початкове повідомлення як fallback, через що воно потрапляє у бандл поруч із перекладом. Intlayer розв'язує резервні переклади під час збирання та надсилає виключно активну мову.
    • Розмір компонента різко зростає у scoped-dynamic. Кожен компонент, скомпільований окремо, важить у середньому 152.6 КБ, оскільки каталог кожного маршруту стає доступним із компонента, який його імпортує. Той самий компонент з useIntlayer() важить у середньому 6.9 КБ.

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

    БібліотекаСтратегіяРозмір Lib (gz)Сер. JS сторінки (gz)Витік локаліВитік сторінкиСер. компонента (gz)Реактивність E2EГідратація
    base (без i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    Linguistatic11.2 KB152.2 KB50.0%90.0%58.0 KB3.9 ms19.9 ms
    Linguidynamic11.2 KB115.2 KB9.3%0.0%85.5 KB5.9 ms28.0 ms
    Linguiscoped-static11.2 KB120.8 KB4.0%0.0%147.9 KB7.1 ms33.9 ms
    Linguiscoped-dynamic11.2 KB120.2 KB8.6%0.0%83.7 KB42.1 ms32.9 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms11.5 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms
    @intlayer/lingui (сумісн.)dynamic10.3 KB137.0 KB9.9%0.0%12.8 KB2.9 ms19.7 ms

    Як інтерпретувати результати

    • За обсягом JavaScript на сторінку Lingui перемагає з мінімальним відривом. dynamic Lingui досягає 115.2 КБ, що на 3.4 КБ менше за Intlayer (118.6 КБ). Скомпільовані каталоги Lingui з хешованими ідентифікаторами дуже компактні, а маршрутизатор TanStack Start настільки добре ділить код, що витік сторінки становить 0% уже в рядку dynamic.
    • Усі інші показники свідчать на користь Intlayer. Гідратація займає 28-34 мс у Lingui проти 11-14 мс у Intlayer: i18n.load() + i18n.activate() запускаються на клієнті до того, як React зможе завершити гідратацію. Компоненти, скомпільовані ізольовано, важать 58-148 КБ проти 6-8 КБ. Витік локалі ніколи не опускається до 0% (становить 4-9%) через наявність рядків вихідної мови.
    • Перемикання локалі в оптимізованій конфігурації відбувається повільно. scoped-dynamic Lingui потребує 42 мс для оновлення html[lang]: каталог нового маршруту необхідно завантажити та активувати до того, як зміна стане помітною. Intlayer перемикається за 3-4 мс в обох режимах.
    • Рядок static в Intlayer вже має 0% витоку сторінки, оскільки до бандла потрапляють лише словники, імпортовані компонентами поточної сторінки. Один рядок конфігурації (importMode: 'dynamic') повністю прибирає і витік локалі.
    • @intlayer/lingui зберігає синтаксис макросів Lingui та обслуговує їх зі словників Intlayer. Це дещо збільшує розмір сторінки (137 КБ через наявність runtime макросів) заради зменшення розміру компонентів (12.8 КБ) та швидшої гідратації, ніж у нативному Lingui. Це чудовий крок для поступової міграції.

    Чому виникає ця різниця? Два компілятори, дві одиниці роботи

    Обидві бібліотеки виконують компіляцію. Різниця полягає в тому, що саме вони компілюють.

    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. Дивіться документацію з оптимізації бандла.

    Досвід розробника

    Налаштування

    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>. Жодних кроків extract чи compile: словники створюються автоматично під час роботи бандлера.

    Компонент

    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. Якщо забути виконати цю команду або compile, застосунок непомітно повернеться до англійського тексту.

    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.

    За межами компонентів

    Метадані, завантажувачі (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, а ваша команда звикла керувати процесом вилучення, компіляції та розділення каталогів. Розмір JS на сторінку дуже конкурентний після налаштування лінивого завантаження.
    • Обирайте Intlayer, якщо вам потрібен контент з прив'язкою до компонентів, суворий TypeScript, помилки про відсутні ключі під час збирання, автоматичний tree-shaking та ліниве завантаження без конфігурації, компактні компоненти, швидка гідратація, миттєве перемикання локалей та вбудовані редакційні інструменти (Візуальний редактор, CMS, переклад за допомогою ШІ, сервер MCP).
    • Обирайте @intlayer/lingui, якщо ви вже використовуєте Lingui і прагнете поступово перейти на словники Intlayer без зміни макросів.

    Схожі порівняння

    Зірки GitHub

    Зірки на GitHub є вагомим показником популярності проєкту, довіри спільноти та його довгострокової актуальності. Хоча вони не є прямою оцінкою технічної досконалості, вони відображають, скільки розробників вважають проєкт корисним і стежать за його розвитком.

    Графік історії зірок

    Висновок

    Lingui є найпотужнішою комбінованою бібліотекою (runtime + компілятор) у цьому бенчмарку. Її скомпільовані хешовані каталоги забезпечують обсяг JavaScript на сторінку лише на кілька кілобайтів більший, ніж у Intlayer, а на TanStack Start навіть дещо менший. Якби розмір сторінки був єдиною метрикою, це була б нічия.

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

    Усі вихідні дані, тестові додатки та скрипти доступні у репозиторії Benchmark Bloom. Ви можете запустити їх самостійно.

    Зверніться до документа 'Чому Intlayer?' для отримання детальнішої інформації.

    Коментарі

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

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

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