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

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

    next-intl на сьогодні є стандартним вибором для i18n у Next.js App Router: тісна інтеграція з маршрутизацією, повна підтримка ICU MessageFormat та звичний досвід розробника для всіх, хто працював із класичними системами i18n.

    Intlayer повністю переосмислює цей підхід: жодних централізованих словників, жодної потреби вручну прив'язувати простори імен (namespaces) до маршрутів. Контент оголошується безпосередньо поруч із кожним компонентом, а компілятор часу збирання пакує лише те, що дійсно потрібно кожній окремій сторінці.

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

    Коротко (tl;dr): next-intl додає щонайменше +12.6 КБ gzip на кожній сторінці лише через свій runtime і призводить до витоку ~90% рядків з інших сторінок у стандартних конфігураціях (static та dynamic). Щоб усунути цей витік, потрібно вручну розбивати каталоги на простори імен та підключати їх окремо для кожної сторінки. Натомість компілятор Intlayer гарантує 0% витоку, втричі менші компоненти та лише +0.3 КБ понад базовий додаток без будь-яких ручних налаштувань.

    Короткий огляд

    • next-intl - Стандарт спільноти Next.js. Централізовані словники JSON для кожної мови, повна підтримка ICU MessageFormat і глибока інтеграція з обробкою запитів та маршрутизацією Next.js.
    • Intlayer - Модель контенту, орієнтована на компоненти. Файли .content.ts розташовані поруч із компонентами, компілятор часу збирання автоматично виконує tree-shaking та ліниве завантаження для кожного компонента і локалі, а також генерує суворі типи TypeScript.
    БібліотекаЗірки GitHubВсього комітівОстанній комітПерша версіяВерсія NPMЗавантаження NPM
    aymericzip/intlayerGitHub Repo starsGitHub commit activityLast CommitКвітень 2024npmnpm downloads
    amannn/next-intlGitHub Repo starsGitHub commit activityLast CommitБерезень 2021npmnpm downloads
    Значки оновлюються автоматично.

    Порівняння функціональності

    ФункціяIntlayer (react-intlayer / next-intlayer)next-intl (next-intl / use-intl)
    Переклади поруч із компонентами✅ Так, .content.ts поруч із кожним компонентом❌ Централізовані словники JSON у папці messages/
    Інтеграція з TypeScript✅ Суворі типи генеруються автоматично з контенту⚠️ Підтримується через ручне налаштування global.d.ts
    Виявлення відсутніх перекладів✅ Помилка TypeScript + помилка/попередження під час збирання⚠️ У runtime повертає ключ або генерує помилку залежно від налаштувань
    Багатий контент (JSX / Markdown / компоненти)✅ Пряма підтримка⚠️ Через t.rich() з передачею компонентів зіставлення
    Підтримка ICU MessageFormat⚠️ У розробці✅ Так, повна підтримка ICU
    Синхронні серверні компонентиuseIntlayer з next-intlayer/server працює у будь-якому дочірньому компоненті❌ Вимагає передачі перекладів через props від асинхронного предка
    Tree-shaking✅ Автоматично для кожного компонента і локалі⚠️ Вимагає ручного поділу на простори імен та використання pick()
    Ліниве завантаження (Lazy loading)✅ Один рядок конфігурації (importMode: 'dynamic')⚠️ Вимагає ручного динамічного імпорту в getRequestConfig
    Візуальний редактор / CMS✅ Безкоштовний Візуальний редактор + додаткова CMS❌ Немає
    Переклад за допомогою ШІ✅ Вбудовано, використовує ваші власні API-ключі❌ Немає
    Сервер MCP та навички агентів✅ Так❌ Немає

    Бенчмарк

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

    Набір тестів 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Завантажується лише активна локаль, але відразу для всіх сторінокБільшість проєктів
    scoped-staticПростори імен на кожен маршрут, без лінивого завантаженняРідко
    scoped-dynamicПростори імен на кожен маршрут + ліниве завантаження. Лише поточна сторінка поточної мовиДодатки з суворим бюджетом продуктивності

    Intlayer не має варіанта "scoped": компілятор автоматично обмежує контекст контенту для кожного компонента, тому рядки static і dynamic уже оптимізовані за маршрутами.

    Для кожного збирання фіксувалося:

    • Розмір бібліотеки (Lib size): розмір gzip порожнього компонента, який імпортує лише бібліотеку i18n.
    • JS сторінки (Page JS): кількість стиснутого gzip JavaScript на сторінку.
    • % витоку локалі (Locale leak %): частка рядків мови, яку користувач не переглядає.
    • % витоку сторінки (Page leak %): частка рядків сторінки, на якій користувач не перебуває.
    • Середній розмір компонента (Component avg): середній розмір gzip кожного компонента, скомпільованого окремо.
    • Реактивність E2E: час між перемиканням мови та оновленням html[lang] у DOM.
    • Гідратація: тривалість фази гідратації React.
    Наведені дані отримані під час тестування від 2026-09-12 з next-intl 4.14.2 та intlayer 9.5.1.

    Результати на Next.js (App Router)

    БібліотекаСтратегіяРозмір Lib (gz)Сер. JS сторінки (gz)Витік локаліВитік сторінкиСер. компонента (gz)Реактивність E2EГідратація
    Базовий додаток (без 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
    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
    @intlayer/next-intl (сумісн.)static8.0 KB147.5 KB0.0%0.0%8.1 KB14.5 ms12.8 ms
    @intlayer/next-intl (сумісн.)dynamic8.0 KB148.7 KB0.0%0.0%8.1 KB11.7 ms12.8 ms

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

    • Витрати runtime. Базовий додаток важить 141.0 КБ на сторінку. next-intl збільшує це значення до 153.6 КБ (+12.6 КБ gzip на кожній сторінці), тоді як Intlayer додає лише 141.3 КБ (+0.3 КБ).
    • Витік контенту. У найпоширеніших конфігураціях (static та dynamic) next-intl відправляє на кожну сторінку ~90% рядків з інших сторінок, оскільки весь файл en.json потрапляє до клієнтського провайдера. Щоб зменшити цей показник до 0%, потрібен складний ручний поділ на простори імен, тоді як в Intlayer це працює за замовчуванням.
    • Розмір компонента. Компонент із useTranslations() важить у середньому 21.8 КБ; той самий компонент з useIntlayer() важить лише 6.9 КБ.

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

    БібліотекаСтратегіяРозмір Lib (gz)Сер. JS сторінки (gz)Витік локаліВитік сторінкиСер. компонента (gz)Реактивність E2E
    Базовий додаток (без i18n)-0.0 KB111.0 KB0.0%0.0%0.7 KB8.1 ms
    use-intlstatic14.1 KB179.8 KB50.0%89.8%76.0 KB6.7 ms
    use-intldynamic14.1 KB119.4 KB0.0%89.8%75.9 KB7.0 ms
    use-intlscoped-static14.1 KB128.7 KB0.0%0.0%87.1 KB20.9 ms
    use-intlscoped-dynamic14.1 KB128.7 KB0.0%0.0%87.1 KB13.3 ms
    intlayerstatic5.0 KB125.8 KB50.0%0.0%8.1 KB3.2 ms
    intlayerdynamic5.0 KB118.6 KB0.0%0.0%6.3 KB3.6 ms
    @intlayer/use-intl (сумісн.)dynamic7.3 KB129.7 KB0.0%0.0%9.3 KB8.7 ms

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

    • Просте налаштування use-intl передає на 68.8 КБ більше JS на сторінку, ніж базовий додаток.
    • У режимі dynamic use-intl досягає 119.4 КБ, але все одно зберігає 89.8% витоку сторінок.
    • Архітектурна різниця особливо помітна у розмірі компонентів: 76-87 КБ у use-intl проти 6-8 КБ в Intlayer.
    • Перемикання локалі відбувається у 2-4 рази швидше з Intlayer (3 мс проти 7-21 мс).

    Чому виникає різниця? Централізовані каталоги проти скомпільованих словників

    next-intl слідує класичній моделі: один JSON для кожної мови, завантажується в getRequestConfig, передається в NextIntlClientProvider і зчитується через t("namespace.key").

    bash
    .
    ├── messages
       ├── en.json
       └── fr.json
    └── src
        ├── i18n
       ├── request.ts
       └── routing.ts
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    └── page.tsx
    

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

    Intlayer змінює цей підхід. Контент декларується безпосередньо поруч із компонентом:

    bash
    .
    ├── intlayer.config.ts
    └── src
        ├── middleware.ts
        └── app
            └── [locale]
                ├── layout.tsx
                └── about
                    ├── page.tsx
                    └── page.content.ts
        └── components
            └── Counter
                ├── index.tsx
                └── index.content.ts
    

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

    Щоб отримати показники рядка dynamic, встановіть dictionary.importMode: 'dynamic' у intlayer.config.ts. Дивіться документацію з оптимізації бандла.

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

    Клієнтський компонент

    next-intl

    messages/en.json
    {
      "counter": {
        "label": "Counter",
        "increment": "Increment"
      }
    }
    
    src/components/Counter.tsx
    "use client";
    
    import { useState } from "react";
    import { useTranslations, useFormatter } from "next-intl";
    
    export const Counter = () => {
      const t = useTranslations("counter");
      const format = useFormatter();
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>{format.number(count)}</p>
          <button aria-label={t("label")} onClick={() => setCount((c) => c + 1)}>
            {t("increment")}
          </button>
        </div>
      );
    };
    

    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
    "use client";
    
    import { useState } from "react";
    import { useIntlayer } from "next-intlayer";
    import { useNumber } from "next-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>
      );
    };
    

    Синхронні серверні компоненти

    Елементи дизайн-системи (навігаційні панелі, підвали, картки) часто є серверними компонентами, що відображаються всередині клієнтських компонентів, тому вони не можуть бути async.

    next-intl

    src/components/ServerCounter.tsx
    type ServerCounterProps = {
      t: (key: string) => string;
      formattedCount: string;
    };
    
    export const ServerCounter = ({ t, formattedCount }: ServerCounterProps) => (
      <div>
        <p>{formattedCount}</p>
        <button aria-label={t("label")}>{t("increment")}</button>
      </div>
    );
    

    Intlayer

    src/components/ServerCounter.tsx
    import { useIntlayer } from "next-intlayer/server";
    import { useNumber } from "next-intlayer/server/format";
    
    export const ServerCounter = ({ count }: { count: number }) => {
      const { label, increment } = useIntlayer("counter");
      const number = useNumber();
    
      return (
        <div>
          <p>{number(count)}</p>
          <button aria-label={label}>{increment}</button>
        </div>
      );
    };
    

    Метадані

    next-intl

    src/app/[locale]/about/page.tsx
    import type { Metadata } from "next";
    import { getTranslations } from "next-intl/server";
    import { routing } from "@/i18n/routing";
    
    const localizedPath = (locale: string, path: string) =>
      locale === routing.defaultLocale ? path : `/${locale}${path}`;
    
    export const generateMetadata = async ({
      params,
    }: {
      params: Promise<{ locale: string }>;
    }): Promise<Metadata> => {
      const { locale } = await params;
      const t = await getTranslations({ locale, namespace: "about" });
    
      const languages = Object.fromEntries(
        routing.locales.map((l) => [l, localizedPath(l, "/about")])
      );
    
      return {
        title: t("title"),
        description: t("description"),
        alternates: {
          canonical: localizedPath(locale, "/about"),
          languages: { ...languages, "x-default": "/about" },
        },
      };
    };
    

    Intlayer

    src/app/[locale]/about/page.tsx
    import { getIntlayer, getMultilingualUrls } from "intlayer";
    import type { Metadata } from "next";
    import type { LocalPromiseParams } from "next-intlayer";
    
    export const generateMetadata = async ({
      params,
    }: LocalPromiseParams): Promise<Metadata> => {
      const { locale } = await params;
      const metadata = getIntlayer("about-metadata", locale);
      const multilingualUrls = getMultilingualUrls("/about");
    
      return {
        ...metadata,
        alternates: {
          canonical: multilingualUrls[locale as keyof typeof multilingualUrls],
          languages: { ...multilingualUrls, "x-default": "/about" },
        },
      };
    };
    

    Збережіть API next-intl, отримайте оптимізований вихід Intlayer

    Вам не потрібно переписувати компоненти, щоб отримати наведені вище результати продуктивності. Пакет @intlayer/next-intl є сумісним адаптером: він зберігає useTranslations, getTranslations, useFormatter, t.rich() та множинні форми ICU, обслуговуючи їх зі словників, скомпільованих компілятором Intlayer.

    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);
    

    У бенчмарку сумісна збірка того самого додатка зменшила розмір сторінки з 153.6 КБ до 147.5 КБ, розмір компонентів з 21.8 КБ до 8.1 КБ, а витік сторінки знизився з ~90% до 0% без внесення змін у код самого додатка. Ваші наявні файли messages/{locale}.json можуть залишатися основним джерелом даних завдяки плагіну синхронізації JSON.

    Дивіться посібник із міграції з next-intl для отримання покрокових інструкцій.

    Що і коли обрати?

    • Обирайте next-intl, якщо вам потрібен стандарт спільноти Next.js, ви повністю спираєтесь на ICU MessageFormat, ваш проєкт невеликий або середній, або ви використовуєте платформи перекладу з централізованими JSON (Crowdin, Phrase, Lokalise...).
    • Обирайте Intlayer, якщо вам потрібен контент з прив'язкою до компонентів, суворий TypeScript, помилки про відсутні ключі під час збирання, автоматичний tree-shaking та ліниве завантаження, синхронні серверні компоненти та вбудовані інструменти редагування (Візуальний редактор, CMS, переклад за допомогою ШІ, сервер MCP).
    • Обирайте @intlayer/next-intl, якщо ви вже використовуєте next-intl і бажаєте отримати переваги в оптимізації бандла без переписування коду.

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

    Зірки GitHub

    Зірки на GitHub є важливим показником популярності проєкту, довіри спільноти та його довгострокової актуальності.

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

    Висновок

    next-intl - це надійна, перевірена часом бібліотека, і бенчмарк підтверджує, що вона залишається хорошим вибором для Next.js. Проте централізована модель каталогів перекладає всю оптимізацію на розробника: стандартне налаштування призводить до витоку близько 90% вмісту інших сторінок, а сам runtime коштує +12.6 КБ gzip на кожній сторінці.

    Intlayer переносить усю цю роботу до компілятора. Словники для кожного компонента, ліниве завантаження для кожної мови та очищення невикористаного контенту стають автоматичними результатами збирання. Результат на тому ж додатку: +0.3 КБ на сторінку, 0% витоку, компоненти втричі менші, а перемикання мови у 2-4 рази швидше на TanStack Start.

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

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

    Коментарі

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

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

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