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

    Lingui проти @intlayer/lingui | Ті самі макроси, інший рантайм

    @intlayer/lingui - це адаптер сумісності (compat adapter) для @lingui/core та @lingui/react. Ваші виклики t`...` , <Trans>, useLingui() та i18n._() залишаються повністю без змін; макроси продовжують компілюватися як зазвичай; єдине, що змінюється - це джерело повідомлень під час виконання (runtime). Замість одного загального скомпільованого каталогу на локаль кожне місце виклику прив'язується до словника Intlayer, скомпільованого персонально для нього.

    У цій статті оцінюється така заміна на одному й тому самому застосунку TanStack Start, зібраному спочатку з чистим Lingui, а потім з адаптером. Дані отримано з Benchmark Bloom. Для безпосереднього порівняння двох бібліотек прочитайте Lingui проти Intlayer. Тут мова піде про те, що саме змінює адаптер і в яких випадках він не надає переваг.

    Стисло (tl;dr): На тому самому застосунку TanStack Start @intlayer/lingui зменшив середній розмір компонента з 85,5 КБ до 12,8 КБ gzip, скоротив час гідратації з 28 мс до 19,7 мс, а перемикання мови - з 5,9 мс до 2,9 мс, не змінюючи макроси. У базовій конфігурації (всі каталоги завантажуються на старті) він також усунув 90% витоку сторінок і заощадив 12 КБ на кожній сторінці. Проте в конфігурації з лінивим завантаженням (lazy loading) він передає 137 КБ на сторінку проти 115 КБ у чистого Lingui: адаптер розбирає синтаксис ICU в рантаймі, тоді як Lingui постачає попередньо скомпільовані масиви токенів. Витік вихідної мови (~9-10%) однаковий з обох боків, оскільки виникає через вбудований у компоненти резервний текст message, а не через рантайм. Адаптер створено як плагін для Vite; вимірювання проводилися на TanStack Start.

    Що являє собою @intlayer/lingui

    Lingui поєднує компілятор і середовище виконання. Макроси у вашому вихідному коді вилучаються в каталог .po (або JSON) для кожної мови, компілюються в окремий JS-модуль на локаль і завантажуються в глобальний екземпляр I18n за допомогою i18n.load() + i18n.activate(). Кожен виклик useLingui() підписується на цей екземпляр; кожен виклик _() шукає свій ідентифікатор в активному каталозі.

    @intlayer/lingui зберігає макроси та API, замінюючи логіку пошуку в каталозі:

    1. Аліаси імпортів (Import aliasing). Плагін lingui() з пакета @intlayer/lingui/plugin огортає vite-intlayer і додає записи resolve.alias, щоб @lingui/core та @lingui/react спрямовувалися на @intlayer/lingui. Імпорти у вашому коді залишаються без змін.
    2. Каталоги як єдине джерело правди. Плагін syncJSON (або syncPO для файлів .po) зчитує наявні каталоги та перетворює їх на словники Intlayer, записуючи переклади назад у файли, коли CLI або CMS вносить зміни. Завдяки параметру splitKeys: "key-prefix" плаский каталог з ідентифікаторами через крапку (footer.github, hero.title) розділяється на невеликі словники за префіксами замість одного важкого файлу розміром 244 КБ.
    3. Прив'язка до місця виклику. Етап оптимізації Intlayer збирає ідентифікатори, передані в _, t та <Trans> у кожному файлі, і надає компоненту лише відповідні словники. <Trans id="hero.title"> прив'язується автономно; useLingui() зв'язується з усіма префіксами, використаними у файлі. Ідентифікатори без крапок (хешовані id, mockBanner) звертаються до єдиного резервного словника messages Lingui.
    src/components/Hero.tsx
    // Ваш код, без змін
    import { useLingui } from "@lingui/react";
    import { Trans } from "@lingui/react/macro";
    
    const Hero = () => {
      const { _ } = useLingui();
      return (
        <section>
          <h1>{_({ id: "hero.title", message: "Measure what you ship" })}</h1>
          <Trans id="hero.subtitle">Every byte counts</Trans>
        </section>
      );
    };
    
    Що створює компілятор (спрощено)
    import _dicHash_hero from "../.intlayer/dictionaries/hero.mjs";
    import {
      useDictionary as useLingui,
      TransDictionary as Trans,
    } from "@intlayer/lingui";
    
    const Hero = () => {
      const { _ } = useLingui(_dicHash_hero);
      return (
        <section>
          <h1>{_({ id: "hero.title", message: "Measure what you ship" })}</h1>
          <Trans id="hero.subtitle" dictionary={_dicHash_hero}>
            Every byte counts
          </Trans>
        </section>
      );
    };
    

    Компонент більше не взаємодіє з глобальним екземпляром і масивним каталогом за ним. Він звертається суто до словника hero. Саме цим зумовлене 7-кратне зменшення розміру компонентів у таблиці нижче.

    Що адаптер зберігає, ігнорує і не замінює

    API LinguiЗ @intlayer/lingui
    Макроси t`...` , msg, plural, select, <Trans>✅ Збережено. Залиште @lingui/babel-plugin-lingui-macro або @lingui/swc-plugin перед кроком Intlayer
    useLingui(){ i18n, _, t }✅ Збережено. Працює і поза Provider (локаль визначається з react-intlayer)
    i18n._(id, values), i18n.t()✅ Збережено. Розв'язує як явні, так і хешовані ідентифікатори
    Форми множини ICU, select, selectordinal, #✅ Збережено, через вбудований резолвер ICU в Intlayer
    i18n.date(), i18n.number(), formats✅ Збережено, на базі нативного Intl
    I18nProvider✅ Збережено. Огортає IntlayerProvider; слухає i18n.on("change") для повторного рендеру за activate()
    i18n.activate(locale)✅ Збережено
    i18n.load(locale, messages) / loadAndActivate()⚠️ Приймається як резервний варіант у рантаймі. Скомпільовані словники мають пріоритет; dev-попередження
    setupI18n({ messages, missing })⚠️ messages об'єднуються як резерв; параметр missing ігнорується
    lingui extract / lingui compile✅ Ваш звичний робочий процес залишається. Спрямуйте syncPO / syncJSON на отримані каталоги
    defaultComponent на I18nProvider⚠️ Зберігається в контексті, але не застосовується під час рендерингу
    Next.js❌ Плагін огортає vite-intlayer. Підтримуються лише Vite, TanStack Start та React Router

    Порівняльний бенчмарк

    Що саме вимірювалося

    Тестовий набір 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.

    Lingui тестувався у чотирьох стратегіях завантаження: від початкового імпорту всіх скомпільованих каталогів (static) до лінивого завантаження каталогу для кожного маршруту (scoped-dynamic). Адаптер тестувався на тих самих компонентах, у яких змінювалися лише файли vite.config.ts та intlayer.config.ts. Рядок static містить усі мови; рядок dynamic (importMode: 'dynamic') завантажує активну мову за вимогою. Окремий варіант "scoped" відсутній, оскільки оптимізатор автоматично ізолює залежності на рівні місць викликів.

    Для кожної збірки фіксуються такі показники:

    • 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/lingui 9.5.1. Тестовий застосунок навмисно компактний (кілька десятків рядків на мову), тому відсотки витоку показують структурну закономірність: вони масштабуються разом зі збільшенням контенту, тоді як базові витрати рантайму залишаються стабільними.

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

    КонфігураціяСтратегіяLib size (gz)Сер. 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 мс
    @intlayer/linguistatic10,3 КБ140,5 КБ50,0%0,0%14,9 КБ3,3 мс11,3 мс
    @intlayer/linguidynamic10,3 КБ137,0 КБ9,9%0,0%12,8 КБ2,9 мс19,7 мс
    intlayer (нативний)static5,0 КБ125,8 КБ50,0%0,0%8,1 КБ3,2 мс11,5 мс
    intlayer (нативний)dynamic5,0 КБ118,6 КБ0,0%0,0%6,3 КБ3,6 мс14,1 мс

    Аналіз отриманих результатів

    • Компоненти: у 7 разів менші. Це головний результат переходу на адаптер. Компонент Lingui, скомпільований ізольовано, займає в середньому від 58 до 148 КБ залежно від стратегії, оскільки useLingui() звертається до глобального екземпляра і кожного завантаженого в нього каталогу. Той самий компонент з адаптером важить у середньому 12,8-14,9 КБ: він підключає лише свої словники та ICU-резолвер.
    • Гідратація: на 8-14 мс швидша. Методи i18n.load() + i18n.activate() виконуються на боці клієнта до того, як React зможе гідратувати інтерфейс. Чим більше Lingui налаштований на ліниве завантаження, тим довше триває цей процес (28-34 мс). З адаптером словники надходять як звичайні імпорти, заздалегідь розміщені бандлером у чанку сторінки: 11,3 мс у режимі static, 19,7 мс у режимі dynamic.
    • Перемикання мови: вдвічі швидше і без ривків. Оптимізована конфігурація scoped-dynamic у Lingui витрачає 42 мс на оновлення html[lang], оскільки каталог маршруту має бути отриманий, завантажений та активований до того, як зміни відобразяться. Адаптер стабільно тримається на рівні 2,9-3,3 мс в обох режимах.
    • Базове налаштування виправляється автоматично. Статичний Lingui доставляє всі каталоги на кожну сторінку: 152,2 КБ та 90% витоку сторінок. Статичний адаптер: 140,5 КБ, 0% витоку сторінок, на тих самих компонентах.
    • Вага кожної сторінки: Lingui виграє в dynamic на 22 КБ. Це важливий нюанс, який слід враховувати. Lingui транслює повідомлення в масиви токенів ще під час збирання й додає легкий рантайм на 11 КБ, який лише проходить по них. Адаптер постачає ICU-резолвер Intlayer (приблизно на 15 КБ більше коду @intlayer/core порівняно з нативною збіркою), шар адаптера (~10 КБ) та react-intlayer (~6 КБ). У даному застосунку це дає 137,0 КБ проти 115,2 КБ. Якщо мінімальна вага кожної сторінки є вашим єдиним пріоритетом і ви вже налаштували Lingui з лінивим завантаженням, адаптер не допоможе зменшити цю цифру.
    • Витік локалі однаковий з обох боків. 9,3% для Lingui та 9,9% для адаптера в режимі dynamic. Причина криється в коді компонентів: виклик i18n._({ id: "careers-benefits.pay", message: "Top-of-market compensation" }) містить англійський оригінал як резервний варіант, так само як і вивід макросів, якщо поле message не видалене. Цей англійський текст потрапляє в чанк fr незалежно від середовища виконання. Нативний Intlayer (.content.ts, де немає вбудованого тексту в коді) досягає 0%.

    Чому змінюються цифри і чому один показник залишається сталим

    Ці значення визначаються двома чинниками: до чого прив'язаний компонент та в якому форматі передаються повідомлення.

    Прив'язка. У Lingui базовою одиницею поділу виступає вся мова цілком. Файл messages.mjs для fr є неподільним модулем; будь-який компонент, що імпортує екземпляр, має доступ до всього змісту, тому бандлер не може розбити його дрібніше за рівень локалі. В адаптері одиницею стає конкретне місце виклику: частини hero та footer є окремими імпортами, які бандлер розділяє й підвантажує для кожного компонента індивідуально. Звідси економія у вазі компонентів, гідратації та усуненні витоку сторінок.

    bash
    .
    ├── lingui.config.ts
    └── src
        ├── i18n.ts                          # setupI18n(), load(), activate()
        ├── locales
       ├── en/messages.mjs              # результат lingui compile, по одному на мову
       └── fr/messages.mjs
        └── components
            └── Hero.tsx                     # useLingui(); _("hero.title")
    
    bash
    .
    ├── intlayer.config.ts                   # syncJSON({ splitKeys: "key-prefix" })
    ├── .intlayer/                           # згенеровано: словник на префікс id для кожної мови
    └── src
        ├── locales
       ├── en/messages.json             # без змін, залишається джерелом правди
       └── fr/messages.json
        └── components
            └── Hero.tsx                     # useLingui(); _("hero.title")  ← без змін
    

    Формат. Компілятор Lingui трансформує вираз {count, plural, one {# item} other {# items}} у масив токенів; рантайму не потрібно парсити синтаксис ICU. Адаптер зберігає повідомлення як текст і розбирає його за допомогою ICU-резолвера Intlayer. Це створює фіксований оверхед близько 15 КБ один раз на сторінку. Саме тому рядок dynamic поступається за обсягом байтів, виграючи за всіма іншими критеріями. Нативний Intlayer уникає цього оверхеду, оскільки в словниках .content.ts використовуються вузли enu() / insert(), попередньо розраховані компілятором.

    Міграція у три кроки

    1. Встановлення

      bash
      npx intlayer init --interactive
      

      Ця команда розпізнає Lingui, аналізує lingui.config.ts для вибору syncPO (для каталогів .po) або syncJSON (для каталогів JSON), встановлює пакети intlayer, react-intlayer, @intlayer/lingui та відповідний плагін синхронізації, а також замінює @lingui/vite-plugin на плагін адаптера у файлі vite.config.ts. Залиште встановленими @lingui/core, @lingui/react та плагін макросів: макроси компілюються як раніше, а адаптер спирається на типи Lingui.

    2. Підключення Intlayer до каталогів

      Для каталогів JSON (коли format: "minimal" у lingui.config.ts):

      intlayer.config.ts
      import { Locales, type IntlayerConfig } from "intlayer";
      import { syncJSON } from "@intlayer/sync-json-plugin";
      
      const config: IntlayerConfig = {
        internationalization: {
          locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
          defaultLocale: Locales.ENGLISH,
        },
        dictionary: {
          importMode: "dynamic",
          format: "icu",
        },
        plugins: [
          syncJSON({
            format: "icu",
            source: ({ locale, key }) => `./src/locales/${locale}/${key}.json`,
            // Групування ідентифікаторів за першим сегментом: `footer.github` → словник `footer`
            splitKeys: "key-prefix",
          }),
        ],
      };
      
      export default config;
      

      Для каталогів .po замініть syncJSON на syncPO з пакета @intlayer/sync-po-plugin з аналогічним шаблоном source та розширенням .po. Дивіться документацію плагіна Sync PO.

      Параметр splitKeys: "key-prefix" є вирішальним фактором для радикального полегшення компонентів. Сам файл каталогу зберігає пласку структуру; поділ існує тільки у згенерованих словниках, а зворотна синхронізація автоматично об'єднує ключі.

    3. Додавання плагіна

      vite.config.ts
      import { defineConfig } from "vite";
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { lingui } from "@intlayer/lingui/plugin";
      
      export default defineConfig({
        plugins: [
          tanstackStart(),
          viteReact({
            // Збережіть плагін макросів; він має виконуватися перед кроком Intlayer
            babel: { plugins: ["@lingui/babel-plugin-lingui-macro"] },
          }),
          lingui(),
        ],
      });
      

      Плагін lingui() об'єднує vite-intlayer (відстеження файлів, компіляція словників, оптимізація) та перенаправляє імпорти @lingui/core і @lingui/react на адаптер. Запустіть збирання проєкту, і наведені вище результати стануть доступними для вашого застосунку.

    Що можна видалити після переходу

    Файл / патернПричина
    await import(`./locales/${locale}/messages.mjs`) | Словники імпортуються компонентами напряму. Метод i18n.load() стає резервним варіантом
    i18n.load() / i18n.loadAndActivate()Залиште i18n.activate(locale); видаліть ручне завантаження каталогів
    lingui compile у сценаріях збиранняТільки якщо ви обрали JSON або .po як джерело і більше не імпортуєте скомпільовані модулі

    Що ви отримуєте крім економії кілобайтів

    • Виявлення пропущених перекладів. Команда npx intlayer test перериває пайплайн CI, якщо для певної мови відсутній ключ; lingui extract лише показує статистичні дані.
    • Автозаповнення через npx intlayer fill. Перекладає відсутні ключі за допомогою обраного AI-провайдера (OpenAI, Anthropic, Mistral, Gemini...) і зберігає їх безпосередньо у ваших каталогах.
    • Візуальний редактор і CMS. Працюють з тими самими словниками, дозволяючи менеджерам редагувати файли .po та JSON через зручний інтерфейс.
    • Поступова міграція на .content.ts. Будь-який компонент можна будь-коли перевести з useLingui() на useIntlayer("hero") з окремим файлом декларації контенту. Обидва типи словників чудово співіснують.

    Обмеження, про які варто знати заздалегідь

    • Накладні витрати на сторінку в режимі dynamic. Як зазначено вище: очікуйте приблизно +20 КБ на сторінку порівняно з лінивим Lingui на невеликому застосунку. Ця різниця не збільшується зі зростанням обсягу контенту (вона зумовлена резолвером, а не каталогами), але й не зменшується.
    • Витік вихідної мови залишається. Дескриптори повідомлень і скомпільовані макроси містять англійський текст як страховку від помилок. Якщо потрібно позбутися цього повністю, доведеться очистити поле message або перевести компонент на .content.ts.
    • i18n.load() є резервним механізмом. Якщо продовжувати імпортувати скомпільовані каталоги та викликати load(), завантажуватимуться і старий, і новий бандли водночас. Видаліть ці імпорти.
    • Лише для екосистеми Vite. Окремого плагіна для Next.js у складі @intlayer/lingui немає. Проєктам на Next.js із Lingui варто відразу розглядати перехід на next-intlayer.
    • Властивість defaultComponent не задіяна. Якщо ви покладалися на неї для автоматичного загортання кожного <Trans>, додайте цей контейнер явно всередині компонентів.

    Що обрати для вашого проєкту?

    • Залишайтеся на Lingui, якщо у вас уже налаштована схема scoped-dynamic, єдиним пріоритетом є мінімальний розмір сторінки в кілобайтах, а перемикання мови за 42 мс і гідратація за 30 мс цілком прийнятні для вашого застосунку.
    • Обирайте @intlayer/lingui, якщо ви використовуєте Lingui та бажаєте отримати легші компоненти, швидку гідратацію та зміну мови, 0% витоку сторінок у базових схемах, типізовані ідентифікатори, перевірки в CI та ШІ-переклад без потреби змінювати макроси. Це ідеальний міст для оновлення поточної кодової бази.
    • Переходьте на нативний Intlayer (react-intlayer), коли почнеться запланований рефакторинг компонентів. Це єдиний варіант у таблиці, що забезпечує 0% витоку локалі, 5 КБ рантайму і лише +7,6 КБ на сторінку порівняно з базовим застосунком.

    Схожі порівняльні огляди

    Висновок

    @intlayer/lingui докорінно змінює те, до чого прив'язуються місця виклику в Lingui: замість глобального екземпляра та монолітного мовного каталогу кожен компонент отримує словник, підготовлений виключно для нього. На тому самому застосунку TanStack Start це забезпечує зменшення компонентів у 7 разів, прискорення гідратації на 8-14 мс, вдвічі швидше перемикання мови та усунення 42-мілісекундної затримки, не вимагаючи редагування макросів. Рішення не змінює резервні тексти всередині компонентів (тому витік вихідної мови залишається) і розбирає ICU на льоту (динамічна схема завантажує приблизно на 20 КБ більше на сторінку, ніж чистий Lingui). Оцініть пріоритетні показники вашого проєкту перед тим, як зробити остаточний вибір.

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

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

    Коментарі

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

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

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