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

    next-intl VS @intlayer/next-intl: Один API, різні Bundle

    next-intl VS Intlayer

    @intlayer/next-intl - це адаптер сумісності: він надає API next-intl (useTranslations, getTranslations, useLocale, t.rich(), ICU plurals, NextIntlClientProvider...) та обслуговує його з словників, скомпільованих Intlayer. Код застосунку не змінюється. Змінюється bundle.

    У цій статті порівнюються обидва на одному й тому ж Next.js застосунку, побудованому один раз з next-intl та один раз з адаптером. Цифри взяті з Benchmark Bloom, open-source набору, який записує, що насправді завантажує браузер. Якщо вам потрібне порівняння next-intl та Intlayer як бібліотек, прочитайте next-intl vs Intlayer. Ця стаття про те, як адаптер змінює все, коли ви зберігаєте компоненти як вони є.

    tl;dr: На тій же Next.js програмі заміна next-intl на @intlayer/next-intl зменшила JavaScript на сторінку з 153.6 KB до 147.5 KB gzip, середній компонент з 21.8 KB до 8.1 KB, витік рядків іноземних сторінок з ~90% до 0% та гідрацію з 14.7 ms до 12.8 ms, без редагування компонентів. На TanStack Start еквівалент use-intl (@intlayer/use-intl) зменшив компоненти з 76-87 KB до 9-11 KB та перемикання локалі з 7-21 ms до 4-9 ms. Адаптер коштує 8.0 KB runtime порівняно з 14.7 KB для next-intl та 5.5 KB для нативного next-intlayer. Навігація та middleware переімплементовані на конфігурації маршрутизації Intlayer; локалізовані pathnames - це одна функція, яка не перенесена.

    Що таке @intlayer/next-intl

    next-intl - це runtime: getRequestConfig завантажує messages/{locale}.json на запит, NextIntlClientProvider відправляє його на клієнт, а useTranslations("about") читає ключі з цього об'єкту під час рендерингу. Кожна оптимізація (namespaces, pick(messages, [...]) на сторінку, ліниве завантаження) - це ваша робота.

    @intlayer/next-intl зберігає першу та останню частину цього ланцюга й замінює середину. Ваші компоненти все ще викликають useTranslations("about"); те, що вони отримують, походить із словника Intlayer, скомпільованого під час збирання, обмеженого цією компонентою, лише в активній мові.

    Три механізми роблять це можливим:

    1. Import aliasing. createNextIntlPlugin() з @intlayer/next-intl/plugin обертає withIntlayer і додає aliases у Webpack / Turbopack, щоб next-intl, next-intl/server, next-intl/navigation та next-intl/middleware були перенаправлені на @intlayer/next-intl. Жоден import у вашій codebase не перейменовується.
    2. JSON як джерело істини. The syncJSON plugin читає ваш існуючий messages/{locale}.json, ділить його top-level ключі на один словник на кожний namespace, і записує переклади назад в ті самі файли, коли CLI або CMS оновлює їх. Робочий процес ваших перекладачів залишається незмінним.
    3. Call-site binding. The Intlayer optimize pass (Babel or SWC) rewrites useTranslations("about") into a call that receives the about dictionary directly. The component no longer reaches a global message tree; it reaches its own content.
    app/[locale]/about/page.tsx
    // Ваш код, без змін
    import { useTranslations } from "next-intl";
    
    const AboutPage = () => {
      const t = useTranslations("about");
      return <h1>{t("title")}</h1>;
    };
    
    What the compiler emits (simplified)
    import _dicHash_about from "../.intlayer/dictionaries/about.mjs";
    import { useDictionary as useTranslations } from "@intlayer/next-intl";
    
    const AboutPage = () => {
      const t = useTranslations(_dicHash_about);
      return <h1>{t("title")}</h1>;
    };
    

    Це переписування - причина, чому колонки розміру компонента та витоку сторінки нижче зміщуються: сторінка завантажує лише словники компонентів, які вона рендерує, і тільки на мові, яка подається.

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

    next-intl APIЗ @intlayer/next-intl
    useTranslations("ns") / getTranslations("ns")✅ Збережено. Прив'язано до словника ns під час збірки. Ключи набиралися на основі вашого вмісту.
    getTranslations({ locale, namespace })✅ Збережено
    t("key", { name }), t.rich(), t.markup(), t.raw()✅ Збережено. ICU plurals, select, selectordinal, #, {ts, date, long} обробляються через ICU resolver Intlayer
    useLocale() / getLocale() / setRequestLocale() / setLocale✅ Збережено
    useFormatter()✅ Збережено. dateTime, number, relativeTime, list, dateTimeRange підтримують нативний Intl
    NextIntlClientProvider✅ Збережено. Props messages, timeZone та now приймаються, але ігноруються (девелопер отримує попередження)
    getMessages()✅ Збережено для сумісності; більше не потрібен
    getRequestConfig() у src/i18n.ts⚠️ Не потрібен. Словники компілюються під час збірки; немає завантаження повідомлень для кожного запиту
    defineRouting()✅ Збережено. Пропущені поля (locales, defaultLocale, localePrefix) читаються з intlayer.config.ts
    createNavigation(), Link, redirect, usePathname, useRouter✅ Збережено. Перереалізовано на базі конфігурації маршрутизації Intlayer; аргумент routing приймається, але ігнорується
    pathnames (локалізовані імена маршрутів)❌ Приймається для типізації, не інтерпольовується. Зберігайте звичайні імена маршрутів або перенесіть це відображення на Intlayer's rewrite
    createMiddleware()✅ Збережено. Повертає проксі Intlayer; встановлює cookie NEXT_LOCALE, щоб useLocale() та ваш перемикач продовжували працювати
    NEXT_LOCALE cookie✅ Читається за замовчуванням (якщо ви не налаштуєте routing.storage самостійно)
    Простий useTranslations() без namespace⚠️ Працює, але місце виклику не пов'язане: розв'язується через реєстр runtime. Передайте namespace для отримання переваг bundle

    Бенчмарк

    Що було вимірено

    Набір 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.

    next-intl було створено з чотирма стратегіями завантаження, від наївної конфігурації (messages/{locale}.json завантажується повністю) до оптимальної (one namespace per route + per-page pick()). Адаптер був створений на тих же компонентах, що й наівна конфігурація, з тільки змінами в next.config.ts та intlayer.config.ts. Він не має варіанту "scoped": компілятор охоплює контент на рівні компонента, тому його рядки static та dynamic вже охоплені.

    Для кожної збірки набір тестів записує:

    • Lib size: gzip розмір порожнього компонента, який лише імпортує бібліотеку i18n. Фіксована вартість runtime.
    • Page JS: gzip JavaScript, завантажений на сторінку, усереднений по всіх сторінках і локалях.
    • Locale leak %: частка перекладених рядків, знайдених у завантаженому JS, які належать мові, яку користувач не переглядає.
    • Page leak %: частка перекладених рядків, знайдених у завантаженому JS, які належать сторінці, на якій користувач не знаходиться.
    • Component avg: середній розмір gzip кожного компонента, скомпільованого окремо. Показує, скільки i18n runtime та каталогу витягує один компонент.
    • E2E reactivity: час від моменту вибору нової мови до оновлення html[lang] у DOM (Playwright, 5 ітерацій).
    • Hydration: тривалість фази React hydration.
    Числа нижче отримані з запуску від 2026-09-12 з next-intl / use-intl 4.14.2 та @intlayer/* 9.5.1. Тестовий застосунок навмисне малий (кілька десятків рядків на кожну мову), тому відсотки витоку описують закономірність: вони зростають з вашим контентом, тоді як витрати runtime залишаються постійними.

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

    Виберіть метрики та бібліотеки, які вас цікавлять:

    Метрика

    Динамічне завантаження JSON

    Ледаче завантаження перекладів під час виконання

    Обмежений JSON (простори імен)

    Простори імен перекладу для кожної сторінки

    Що це за метрика?

    Загальний стиснений у gzip розмір пакета бібліотеки інтернаціоналізації. Він включає лише провайдер та логіку отримання контенту після tree-shaking та мініфікації.

    Чому це важливо?

    Менший розмір бібліотеки зменшує початкове завантаження JavaScript, що призводить до швидшого завантаження та виконання на клієнті.

    Перегляд як

    SetupStrategyLib size (gz)Page JS avg (gz)Locale leakPage leakComponent avg (gz)E2E reactivityHydration
    base (no i18n)-0.0 KB141.0 KB0.0%0.0%0.9 KB13.4 ms11.8 ms
    next-intlstatic14.7 KB153.6 KB4.2%89.8%21.8 KB16.0 ms14.7 ms
    next-intldynamic14.7 KB153.6 KB9.7%89.9%21.8 KB15.6 ms14.8 ms
    next-intlscoped-static14.7 KB153.6 KB0.0%0.0%80.1 KB17.9 ms17.4 ms
    next-intlscoped-dynamic14.7 KB153.6 KB0.0%0.0%22.9 KB17.8 ms16.8 ms
    @intlayer/next-intlstatic8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intldynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms
    next-intlayer (native)static5.5 KB141.3 KB0.0%0.0%8.5 KB15.5 ms16.9 ms
    next-intlayer (native)dynamic5.5 KB141.3 KB0.0%0.0%6.9 KB15.3 ms15.9 ms

    Як це читати

    • Ті самі компоненти, на 6 KB менше на сторінку. Адаптер build наївної програми приходить до 147.5 KB, менше ніж будь-яка конфігурація next-intl, включаючи повністю оптимізовану (153.6 KB). Сам runtime є різницею: 8.0 KB проти 14.7 KB, сплачується на кожній сторінці.
    • Витік коду йде до 0% без змін компонентів. Наївне налаштування next-intl поставляє ~90% рядків іноземних сторінок на кожну сторінку. Досягнення 0% за допомогою next-intl означає налаштування scoped-*: один простір імен на маршрут і pick(messages, [...]) на кожній сторінці. Адаптер досягає 0% з наївного коду, оскільки крок оптимізації прив'язує кожен useTranslations("ns") до свого словника.
    • Компоненти зменшуються в 2,7 разу. Компонент, скомпільований окремо, в середньому становить 21,8 KB з next-intl (він досягає провайдера та дерева повідомлень) і 8,1 KB з адаптером. У налаштуванні scoped-static next-intl це число зростає до 80 KB, оскільки файл простору імен кожного маршруту стає доступним зі сторінки, яка його вибирає.
    • Гідрація на 2 мс швидша (12.8 vs 14.7 мс): немає об'єкта повідомлень для десеріалізації з RSC payload перед тим, як React може гідрувати.
    • Адаптер - це не native runtime. next-intlayer займає 141.3 KB, +0.3 KB понад базову програму, з 5.5 KB runtime. Адаптер несе поверхню API next-intl (useFormatter, t.rich, ICU resolver) поверх ядра Intlayer, звідси 8.0 KB і +6 KB за сторінку. Це міст, а не пункт призначення.
    Повна таблиця, кожна бібліотека та стратегія у звіті про бенчмарк Next.js.

    Результати на TanStack Start (use-intl)

    use-intl - це framework-агностичне ядро next-intl. Його адаптер @intlayer/use-intl слідує тому ж дизайну з Vite плагіном (@intlayer/use-intl/plugin).

    НалаштуванняСтратегіяРозмір Lib (gz)Page JS середньо (gz)Витік локаліPage витікComponent середньо (gz)E2E реактивністьГідратація
    base (без i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms21.6 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms15.3 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms15.4 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms24.8 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms25.9 ms
    @intlayer/use-intlstatic7.3 KB135.8 KB49.7%0.0%10.9 KB4.2 ms10.5 ms
    @intlayer/use-intldynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms16.1 ms
    intlayer (native)static5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms11.5 ms
    intlayer (native)dynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms14.1 ms

    Як читати це

    • Байти на сторінку практично однакові з оптимізованим use-intl. @intlayer/use-intl у режимі dynamic (129.7 KB) знаходиться в межах 1 KB від use-intl's scoped-dynamic (128.7 KB) і на 10 KB вище звичайного dynamic від use-intl (119.4 KB). Той звичайний рядок dynamic все ще витікає 90% рядків зі сторінок інших мов; кількість байтів низька, тому що тестовий додаток має малий обсяг вмісту. Адаптер 0% залишається постійним у міру зростання вмісту.
    • Компоненти мають розмір у 7-9 разів менший. Компоненти use-intl мають середній розмір 76-87 KB у кожній стратегії, оскільки useTranslations привʹязаний до цілого об'єкту message провайдера. Адаптер має середній розмір 9-11 KB.
    • Переключення локалі працює швидше. Оптимізовані налаштування use-intl потребують 13-21 мс для оновлення html[lang]; адаптер потребує 4-9 мс. Менше компонентів перерендерюється, і ніщо не переважається із дерева повідомлень.
    • static зберігає кожну локаль. Рядок адаптера static показує витік локалі 49.7%, той же, що й у нативному Intlayer у режимі static: усі локалі bundled, але лише словники сторінки. Один рядок конфігурації (importMode: 'dynamic') видаляє його.
    Повна таблиця у звіті про бенчмарк TanStack Start.

    Чому змінюються цифри

    The Intlayer compiler extracts content from components

    Ніщо в компоненті не змінилось, тому виграш повністю походить від того, до чого привʹязаний useTranslations.

    З next-intl, binding це провайдер. NextIntlClientProvider отримує весь об'єкт messages для локалі; кожен useTranslations("about") читає з нього. Bundler бачить один компонент, що імпортує один hook, який читає один контекст, і не може знати, що використовується тільки гілка about. Маршрути нижче всі ділять один і той же об'єкт messages, тому колонка page-leak читає ~90%, доки ви самі не розділите файл, і зайве навантаження зростає за двома осями одночасно, сторінками та мовами:

    Theoretical content leakage by architecture

    bash
    .
    ├── messages
    │   ├── en.json                       # кожен namespace, кожна сторінка
    │   └── fr.json
    └── src
        ├── i18n.ts                       # getRequestConfig({ messages: await import(...) })
        ├── middleware.ts                 # createMiddleware(routing)
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider messages={messages}>
            └── about/page.tsx            # useTranslations("about")
    

    З @intlayer/next-intl, привʼязка - це словник. syncJSON перетворює messages/en.json на один словник для кожного ключа верхнього рівня; компілятор визначає, який компонент викликає useTranslations("about"), і передає йому about безпосередньо, активною мовою, як імпорт, який bundler може розпізнати та розділити.

    bash
    .
    ├── intlayer.config.ts                # syncJSON({ source: ({ locale }) => `./messages/${locale}.json` })
    ├── messages
    │   ├── en.json                       # unchanged, still the source of truth
    │   └── fr.json
    ├── .intlayer/                        # generated: one dictionary per namespace, per locale
    └── src
        ├── middleware.ts                 # createMiddleware() тепер повертає proxy Intlayer
        └── app/[locale]
            ├── layout.tsx                # <NextIntlClientProvider> (без props messages)
            └── about/page.tsx            # useTranslations("about")  ← без змін
    

    src/i18n.ts та props messages зникають. Все інше залишається ідентичним.

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

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

      bash
      npx intlayer init --interactive
      

      Команда виявляє next-intl і встановлює intlayer, next-intlayer, @intlayer/next-intl та @intlayer/sync-json-plugin. Тримайте next-intl встановленим: це peer dependency адаптера і надає типи.

    2. Вкажіть Intlayer на ваші повідомлення

      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: {
          // "static" об'єднує кожну локаль; "dynamic" завантажує активну на вимогу
          importMode: "dynamic",
        },
        plugins: [
          syncJSON({
            // ICU placeholders: {name}, {count, plural, one {# item} other {# items}}
            format: "icu",
            source: ({ locale }) => `./messages/${locale}.json`,
            location: "messages",
          }),
        ],
      };
      
      export default config;
      

      messages/{locale}.json залишається на місці. Кожен ключ верхнього рівня стає словником; useTranslations("about") відображається на словник about.

    3. Обгорнути next.config.ts

      next.config.ts
      import type { NextConfig } from "next";
      import { createNextIntlPlugin } from "@intlayer/next-intl/plugin";
      
      const withIntlayer = createNextIntlPlugin();
      
      const nextConfig: NextConfig = {};
      
      export default withIntlayer(nextConfig);
      

      createNextIntlPlugin() компонує withIntlayer (спостереження за контентом, компіляція словників, оптимізаційний прохід) та next-intl → @intlayer/next-intl aliases для Webpack та Turbopack. Збуйте, і цифри в таблицях вище будуть вашими.

    Що ви можете видалити потім

    Файл / шаблонЧому
    getRequestConfig в src/i18n.tsНемає завантаження повідомлень за запитом. Збережіть файл лише якщо він також експортує createNavigation помічники
    messages={...} на NextIntlClientProviderАдаптер читає скомпільовані результати; властивість ігнорується та логує попередження під час розробки
    await getMessages() у layoutsЗ тієї ж причини
    Per-page pick(messages, [...])Компілятор виконує вибір для кожного компонента

    Що ви отримуєте крім зменшення розміру

    • Типізовані ключі. useTranslations("about") типізована проти скомпільованого словника about. t("does.not.exist") - це помилка TypeScript, а не fallback під час виконання.
    • npx intlayer test не проходить CI, коли локалі не вистачає ключа. npx intlayer fill перекладає відсутні за допомогою обраного провайдера (OpenAI, Anthropic, Mistral, Gemini...) використовуючи ваш власний ключ, і записує результат назад у messages/{locale}.json.
    • Visual Editor і CMS працюють з одними й тими ж словниками, тому розробники, що не є розробниками, можуть редагувати messages/fr.json через UI і файл оновлюється.
    • Постійний перехід до .content.ts. Будь-який компонент може перейти з useTranslations("about") на useIntlayer("about") з co-located файлом вмісту, один за одним. JSON і .content.ts словники співіснують і об'єднуються.

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

    createNavigation(routing) та createMiddleware(routing) зберігають сигнатуру, але ігнорують аргумент: локалі, локаль за замовчуванням та стратегія префіксів надходять із конфігурації routing Intlayer. Якщо ви використовуєте локалізовані pathnames у next-intl (/about до /a-propos), адаптер їх не інтерполює; routing.rewrite в Intlayer покриває цей випадок, але це окрема зміна.

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

    8.0 KB рантайму проти 5.5 KB для next-intlayer, і +6-7 KB на сторінку порівняно з нативною збіркою. Це плата за інтерфейс API next-intl. Коли кожен компонент перейде на useIntlayer, видаліть адаптер.

    Форматери базуються на нативному Intl, і лише локаль впливає на їх результат. Якщо для стабільної гідратації дат ви покладаєтеся на примусовий часовий пояс або фіксований now, обробляйте це у місці виклику. Див. форматування дати, часу та чисел.

    Коли використовувати що?

    Ваш застосунок невеликий, розмір бандла вас не турбує, а команді зручно вручну керувати просторами імен і pick() для кожної сторінки.

    Ви вже використовуєте next-intl сьогодні й бажаєте отримати переваги у розмірі бандла, відсутності витоків та швидкій гідратації, типізованих ключах та інструментах CLI / CMS без переписування коду. Це рекомендована точка входу для будь-якої існуючої кодової бази next-intl.

    Для нових проєктів або коли адаптер виконав свою перехідну роль. Це найлегший із трьох варіантів (5.5 KB, +0.3 KB на сторінку), що відкриває синхронні серверні компоненти, файли .content.ts для кожного компонента та повний набір функцій. Почніть з Intlayer з Next.js.

    FAQ

    У Next.js так для компонентів: збірка бенчмарку змінила лише next.config.ts та intlayer.config.ts. getRequestConfig у src/i18n.ts, проп messages у провайдері та виклики pick() стають мертвим кодом, який ви можете згодом видалити.

    Вони продовжують працювати. t("key", { count }), t.rich(), t.markup(), select, selectordinal, # та {ts, date, long} обробляються резолвером ICU в Intlayer. Див. формат повідомлень ICU.

    Він несе API next-intl поверх ядра Intlayer: useFormatter, t.rich, резолвер ICU, хелпери навігації. Це 8.0 KB проти 5.5 KB і +6 KB на сторінку. Це міст, а не кінцева мета.

    Так. Будь-який компонент може перейти з useTranslations("about") на useIntlayer("about") з розташованим поруч файлом .content.ts. Словники JSON та .content.ts співіснують і об'єднуються.

    Не через pathnames у next-intl: адаптер приймає їх для типізації, але не інтерполює. Використовуйте натомість routing.rewrite від Intlayer.

    Пов'язані порівняння

    Та сама серія адаптерів:

    Пряме порівняння бібліотек:

    Довідкова документація:

    Щоб зрозуміти, звідки взялися ці бібліотеки, прочитайте історію i18n у JavaScript.

    Висновок

    @intlayer/next-intl робить одне: змінює те, до чого привязується useTranslations, від provider'а, який містить кожне повідомлення, до dictionary, скомпільованого для цього компонента. На тій же Next.js додатку, що коштує 6 KB на сторінку, компоненти в 2.7x менші, 0% витоку та 2 ms гідратації, перед тим, як хтось відкриє файл компонента. Navigation і middleware зберігають свій API поверх конфігу маршрутизації Intlayer, а нативний next-intlayer runtime залишається ще легшим.

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

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

    Коментарі

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

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

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