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

    Как выбрать подходящую библиотеку i18n для Vue

    «Vue i18n», это одновременно и общий термин, и название библиотеки, которую устанавливают почти все. Это удобно и в то же время вводит в заблуждение: vue-i18n, отличный вариант по умолчанию, но далеко не единственный, а вопросы, определяющие выбор (нужен ли SSR, сколько страниц, кто пишет переводы), редко задаются до выполнения npm install.

    Это руководство сначала ставит правильные вопросы, а затем сопоставляет ответы с подходящими библиотеками, как для чистого Vite + Vue, так и для Nuxt.

    Экосистема библиотек i18n для Vue

    Table of Contents

    Шесть вопросов перед сравнением библиотек

    1. Vite SPA или Nuxt? В SPA затраты на каталог, это проблема размера JS bundle. В Nuxt это также проблема размера HTML payload, поскольку сообщения сериализуются в состояние SSR и гидратируются. Именно поэтому большинство жалоб на то, что «vue-i18n работает медленно», исходят от приложений на Nuxt.
    2. Кто пишет переводы? Разработчики, TMS, агентство, поставляющее строки ICU, или AI-пайплайн. vue-i18n использует собственный синтаксис плюрализации, разделенный вертикальной чертой (pipe), а не ICU. Это имеет решающее значение, если строки поступают извне.
    3. Сколько локалей и страниц? Две локали и пять страниц могут позволить себе загружать всё сразу. Десять локалей и сорок маршрутов не могут, и стратегия загрузки становится главной статьей расходов.
    4. Нужна ли типизация ключей? t("cart.totl") скомпилируется в vue-i18n без ошибок, если не передать generic схемы сообщений, а эта схема конфликтует с лениво загружаемыми каталогами.
    5. Что содержит контент? Только UI-метки или также markdown, ссылки внутри предложений и блоки под конкретные локали. Сложный (rich) контент, это место, где t(), возвращающий строку, становится неудобным.
    6. Является ли CSP ограничением? Сборка vue-i18n по умолчанию компилирует сообщения в браузере с помощью new Function. Сборкам только для runtime требуется @intlify/unplugin-vue-i18n для предварительной компиляции во время сборки.

    Запишите ответы. Всё изложенное ниже будет опираться на них.

    Общая картина

    В экосистеме Vue меньше библиотек i18n, чем в React, и они относятся к разным архитектурным волнам.

    История библиотек i18n в JavaScript

    vue-i18n появился в 2015 году и с тех пор остается решением по умолчанию. @nuxt/i18n оборачивает его, добавляя маршрутизацию локалей, SEO-теги и ленивую загрузку для каждой локали. Сообщения компилируются в функции рендеринга: во время сборки (если подключен unplugin) или в браузере.

    Файлы .ftl от Mozilla Fluent принесли более удобный синтаксис сообщений с учетом грамматических вариантов. Типизация ключей отсутствует, а плагин Vite загружает все локали на каждую страницу.

    Paraglide генерирует отдельную функцию для каждого сообщения и позволяет сборщику исключать неиспользуемый код через tree-shaking. Intlayer объявляет контент для каждого компонента в файлах .content.ts, генерирует типы и отправляет клиенту только то, что рендерит маршрут.

    В статье об истории JavaScript i18n каждая волна рассмотрена подробно.

    Главное решение: где живет контент и когда он загружается

    Два структурных выбора объясняют большую часть разницы в размере bundle между различными конфигурациями:

    • Централизованный или локальный (scoped) контент. Один locales/en.json на всё приложение или отдельное объявление для каждого компонента.
    • Статический или динамический импорт. Загрузка всего сразу при запуске или получение активной локали (и в идеале активного маршрута) по требованию.

    На графике показана расчетная нагрузка для теоретического приложения от 1 до 10 страниц, переведенного на 1–10 локалей, при объеме текста около 30 КБ на страницу.

    Теоретическая утечка контента в зависимости от архитектуры

    vue-i18n поддерживает динамическую ось: вызов setLocaleMessage после import() позволяет не отправлять девять локалей, которые никто не читает. Однако он не дает разделения по страницам. Каталог локали, это один объект, и его загрузка подтягивает текст для всех страниц. В SPA этого никто не заметит. В Nuxt с @nuxtjs/i18n и более чем десятью страницами каждый маршрут несет в себе строки всех остальных маршрутов дважды: в JS chunk и в SSR payload.

    В бенчмарке Vue это измеряется как «утечка из других маршрутов» и «утечка из других локалей». Если вашим ответом на вопрос 3 было «много страниц», этот раздел важнее любых предпочтений по API. В статье о покомпонентном и централизованном i18n этот компромисс рассматривается со стороны поддержки кода.

    Кандидаты

    Размеры библиотек взяты из бенчмарка Vue: плагин плюс composable в пустом компоненте после сборки, tree-shaking и минификации для приложения из 10 страниц и 10 локалей. Контент измеряется отдельно.

    БиблиотекаМодель контентаТипы на ключахФормат сообщенийРазделение по маршрутамРазмер библиотеки
    vue-i18nЦентральные каталоги на локаль, опционально блоки SFC <i18n>Опционально через generic схемыСобственный (pipe plurals)Нет~24.3 kB
    @nuxtjs/i18nКак и в vue-i18n, плюс маршрутизация и SEO-тегиТо жеТо жеНет, только по локалямПоверх vue-i18n
    fluent-vueФайлы .ftl (Mozilla Fluent)НетFluentНет~29.7 kB
    ParaglideПроект inlang, сгенерированные функцииСгенерированныеСобственныйЧерез tree-shakingОколо нуля
    IntlayerОдин .content.ts на компонентСгенерированные, по умолчаниюХелперы (plural)Да, по компонентамБазовый уровень
    Значения представляют собой срез версий на момент проведения бенчмарка. Запустите его на своем приложении, прежде чем принимать решение исключительно на основе размера.

    Размер библиотеки Paraglide близок к нулю по архитектурным причинам: runtime генерируется прямо в ваш репозиторий, что требует шага регенерации перед каждым push и приводит к merge-конфликтам в сгенерированных файлах. Intlayer требует vite-intlayer (или модуль для Nuxt), поэтому не может работать без шага сборки.

    Сопоставьте ваши ответы с библиотекой

    vue-i18n в режиме Composition (legacy: false) с @intlify/unplugin-vue-i18n, чтобы использовать сборку только для runtime. Ленивая загрузка локалей через import(). Это покрывает большинство небольших приложений, а ответы сообщества можно найти везде. Блоки SFC <i18n> размещают сообщения рядом с компонентом, что удобно, однако экосистема извлечения и TMS-инструментов вокруг них слабее, чем вокруг JSON-каталогов, поэтому заранее определитесь, что будет использовать команда.

    @nuxtjs/i18n предоставляет стратегию маршрутизации, теги hreflang и определение локали без написания лишнего кода, и одного этого достаточно для контентных сайтов с небольшим числом страниц. Его ограничением является каталог на уровне локали: когда страниц больше десятка, SSR payload начинает содержать тексты всех маршрутов. Если это ваш случай, настройте vue-i18n вручную с сообщениями для каждого маршрута либо перейдите на scoped-контент. В статье об i18n в Nuxt подробно рассматривается выбор стратегии маршрутизации.

    Синтаксис плюрализации в vue-i18n ("no item | one item | {count} items"), это не ICU, и он не является переносимым. Переводчиков нужно предупреждать об этом заранее, а экспорт из TMS не выдаст такой формат автоматически. Либо согласуйте формат до создания первого каталога, либо выберите библиотеку, формат которой совпадает с форматом вашего поставщика. Поддержка ICU в Intlayer частичная, поэтому если вы уже получаете строки в ICU, учитывайте это как сдерживающий фактор.

    Отдавайте предпочтение scoped-контенту, скомпилированному во время сборки. Paraglide решает эту задачу через tree-shaking, который отлично работает на Vite. Intlayer достигает этого за счет покомпонентных объявлений и отправляет клиенту только то, что рендерит маршрут. В vue-i18n можно разделять сообщения по маршрутам вручную, но инструменты этого не контролируют, и общий компонент, импортирующий глобальный namespace, незаметно нарушит разделение.

    vue-i18n можно типизировать, передав generic схемы в createI18n. Это работает, но ломается при ленивой загрузке каталогов, так как схема описывает сообщения, которых может еще не быть в памяти. Если вы не хотите поддерживать это вручную, выберите библиотеку, где типы генерируются из контента: Paraglide или Intlayer. В статье об обнаружении недостающих переводов сравнивается, что каждая библиотека находит на этапе сборки.

    Markdown-страницы, предложения с <RouterLink> посередине, компоненты под конкретные локали. В vue-i18n есть <i18n-t> для интерполяции компонентов, что работает, но выглядит многословно. Узлы контента в Intlayer принимают markdown, HTML и вложенные объекты напрямую, что гораздо удобнее для приложений с большим объемом контента.

    В этом случае у централизованного JSON не остается потребителя, который бы его оправдывал. Колоцированный контент в сочетании с CLI, дополняющим недостающие локали, наиболее прямой путь. Команда fill в Intlayer работает с вашим собственным API-ключом (OpenAI, Anthropic, Mistral, Gemini) и переводит только то, что изменилось.

    В чем слабые стороны каждой библиотеки

    • vue-i18n: самая тяжелая из всех, собственный формат плюрализации, типы подключаются вручную и нестабильны при ленивой загрузке, нет разделения по маршрутам, неиспользуемые ключи незаметно накапливаются. Оставленный флаг legacy: true в приложении Vue 3 сохраняет слой совместимости с Vue 2 и отключает типизацию useI18n().
    • @nuxtjs/i18n: наследует все недостатки выше, а SSR payload несет в себе строки каждой страницы, как только число маршрутов превышает десяток.
    • fluent-vue: приятный синтаксис сообщений, отсутствие типизации ключей, а плагин Vite загружает весь контент на всех языках на каждую страницу. Самая тяжелая библиотека в бенчмарке.
    • Paraglide: сгенерированные файлы сохраняются в репозиторий, требуется регенерация перед каждым push, а локаль считывается из cookie или storage при каждом вызове сообщения вместо реактивного store, что создает лишнюю нагрузку при смене языка.
    • Intlayer: обязательный плагин для сборки, меньшая экосистема, частичная поддержка ICU, контент распределен по кодовой базе по концепции дизайна, поэтому для экспорта единого JSON для переводчика требуются специальные инструменты.

    Как каждый вариант выглядит в коде

    Один и тот же компонент, сводка корзины с заголовком и формой множественного числа, написанный с использованием каждого кандидата. Самое интересное здесь, не шаблон, а то, где находится контент и что о нем знает vue-tsc.

    src/locales/en.json
    {
      "cart": {
        "title": "Your cart",
        "items": "no item | one item | {count} items"
      }
    }
    
    src/components/CartSummary.vue
    <script setup lang="ts">
    import { useI18n } from "vue-i18n";
    
    const props = defineProps<{ count: number }>();
    const { t } = useI18n();
    </script>
    
    <template>
      <section>
        <h2>{{ t("cart.title") }}</h2>
        <p>{{ t("cart.items", { count: props.count }, props.count) }}</p>
      </section>
    </template>
    

    Плюрализация через вертикальную черту (pipe), собственный формат vue-i18n, а не ICU. Функция t принимает любую строку, если не передать generic схемы сообщений в createI18n.

    src/locales/en.ftl
    cart-title = Your cart
    cart-items = { $count ->
        [one] { $count } item
       *[other] { $count } items
    }
    
    src/components/CartSummary.vue
    <script setup lang="ts">
    import { useFluent } from "fluent-vue";
    
    const props = defineProps<{ count: number }>();
    const { $t } = useFluent();
    </script>
    
    <template>
      <section>
        <h2>{{ $t("cart-title") }}</h2>
        <p>{{ $t("cart-items", { count: props.count }) }}</p>
      </section>
    </template>
    

    Синтаксис Fluent отлично справляется с формами множественного числа и грамматическими вариантами. Идентификаторы сообщений, это нетипизированные строки, а плагин Vite включает все локали в каждую страницу.

    messages/en.json
    {
      "cart_title": "Your cart",
      "cart_items": "{count} items"
    }
    
    src/components/CartSummary.vue
    <script setup lang="ts">
    import { m } from "../paraglide/messages.js";
    
    const props = defineProps<{ count: number }>();
    </script>
    
    <template>
      <section>
        <h2>{{ m.cart_title() }}</h2>
        <p>{{ m.cart_items({ count: props.count }) }}</p>
      </section>
    </template>
    

    Каждое сообщение, это сгенерированная типизированная функция, поэтому отсутствие ключа вызывает ошибку импорта. Папка paraglide/ генерируется прямо в репозитории и пересоздается при каждом изменении.

    src/components/cartSummary.content.ts
    import { plural, t, type Dictionary } from "intlayer";
    
    const cartSummaryContent = {
      key: "cart-summary",
      content: {
        title: t({
          ru: "Ваша корзина",
          en: "Your cart",
          fr: "Votre panier",
          es: "Tu carrito",
        }),
        items: t({
          ru: plural({
            one: "{{count}} товар",
            few: "{{count}} товара",
            many: "{{count}} товаров",
            other: "{{count}} товаров",
          }),
          en: plural({ one: "{{count}} item", other: "{{count}} items" }),
          fr: plural({ one: "{{count}} article", other: "{{count}} articles" }),
          es: plural({ one: "{{count}} artículo", other: "{{count}} artículos" }),
        }),
      },
    } satisfies Dictionary;
    
    export default cartSummaryContent;
    
    src/components/CartSummary.vue
    <script setup lang="ts">
    import { useIntlayer } from "vue-intlayer";
    
    const props = defineProps<{ count: number }>();
    const { title, items } = useIntlayer("cart-summary");
    </script>
    
    <template>
      <section>
        <h2><title /></h2>
        <p>{{ items(props.count) }}</p>
      </section>
    </template>
    

    Все локали находятся в одном файле рядом с компонентом. Типы генерируются во время сборки, поэтому для title работает автодополнение, а опечатка вызовет ошибку vue-tsc. <title /> рендерит узел, доступный визуальному редактору, а {{ items(props.count) }} возвращает обычную строку.

    Уже используете vue-i18n? Адаптер совместимости @intlayer/vue-i18n создает псевдонимы пакета на уровне сборщика, благодаря чему useI18n(), $t, pipe-плюрализация и v-t продолжают работать, пока Intlayer управляет контентом. В руководстве по миграции описан последующий отказ от адаптера, также доступно руководство для Nuxt.

    Перед тем как сделать выбор

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

    Проверьте активность репозитория.

    Коммиты, время ответа на issues и выходил ли последний минорный релиз в этом году. Надежная архитектура без мейнтейнера, это отложенная миграция.

    Не выбирайте исключительно по числу загрузок в npm.

    Самая скачиваемая библиотека, это та, что вышла первой, а не та, которая лучше всего подходит для кодовой базы на Vue в 2026 году. Число загрузок отражает историю, а не соответствие текущим требованиям.

    Рейтинг библиотек i18n для JavaScript

    Узнайте, кто финансирует мейнтейнера и что они продают.

    vue-i18n поддерживается платформой Crowdin, как next-intl и svelte-i18n. i18next поддерживается Locize. Tolgee, Paraglide (inlang) и Intlayer развивают собственные платформы. Поставщик, чей доход строится на платном хостинге переводов, мало заинтересован в том, чтобы переводы стали бесплатными внутри вашего toolchain. Intlayer, единственный вариант из списка, предлагающий AI-перевод через CLI с вашим собственным API-ключом, а также CMS, которую можно развернуть самостоятельно (self-host).

    Готова ли библиотека к работе с AI-агентами?

    Агенты все еще испытывают сложности с i18n: они забывают локали, выдумывают ключи и путают синтаксис сообщений. Поставляет ли библиотека Agent Skills или MCP-сервер, чтобы агент мог получать список контента, заполнять его и тестировать? Оптимизирована ли загрузка контента по умолчанию, или кому-то придется каждый квартал проверять namespaces и ленивые импорты?

    Типобезопасность из коробки.

    Не «можно типизировать с помощью дополнительных настроек», а «неверный ключ приводит к ошибке tsc сразу после чистой установки». Проверьте, что происходит при обращении к несуществующему ключу и если для какой-то локали пропущен перевод.

    Обнаружение неиспользуемого контента.

    Каталоги со временем только растут. Сборка Intlayer удаляет неиспользуемые поля и логирует их (build.purge). Paraglide решает эту задачу на уровне архитектуры, поскольку невызываемая функция сообщения удаляется через tree-shaking. Во всех остальных случаях очистка остается за вами.

    Developer experience.

    Время от настройки до первой переведенной строки, наличие LSP или расширения для VS Code, показывающего перевод при наведении и позволяющего перейти к объявлению, CLI для заполнения, тестирования и отправки, а также возможность для не-разработчиков редактировать контент (визуальный редактор или CMS) без создания pull request.

    Часто задаваемые вопросы

    Для большинства приложений на Vue, да. У нее крупнейшая экосистема, подробная документация и предсказуемые издержки: тяжелый runtime, собственный формат множественного числа и разделение по маршрутам, которое вам придется настраивать и контролировать самостоятельно.

    Используйте модуль, если только у вас не нестандартная маршрутизация или в приложении мало страниц. Настройка вручную означает самостоятельную реализацию маршрутов локалей, middleware, hreflang и карты сайта (sitemap), а это сложнее, чем кажется.

    Только если размер bundle, SSR payload, сгенерированные типы или проверка отсутствующих ключей во время сборки являются обязательными требованиями. В статье о сравнении компиляторного и декларативного подходов в i18n объясняется, что дают компиляторы и где они могут ошибаться.

    Косвенно. Поисковым роботам важны маршрутизация, hreflang, <html lang> и наличие текста в HTML, отрендеренном на сервере. См. руководство по hreflang.

    Дополнительные материалы

    Комментарии

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

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

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