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

    Vite i18n: особенности сборщика, а не вашего фреймворка

    Большинство руководств по «Vite i18n» на самом деле представляют собой статьи по React или Vue, в которых случайно используется Vite. Этот материал посвящен уровню ниже: как импортируются каталоги, что с ними делает Rollup и почему написанная вами ленивая загрузка (lazy loading), скорее всего, таковой не является.

    Содержание

    Статический импорт по умолчанию синхронен и жаден

    Самая базовая конфигурация импортирует все каталоги в верхней части модуля:

    src/i18n.ts
    import en from "./locales/en.json";
    import fr from "./locales/fr.json";
    import ja from "./locales/ja.json";
    

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

    import.meta.glob и флаг, в котором все ошибаются

    Glob-импорт в Vite обычно решает эту проблему:

    ts
    const catalogs = import.meta.glob("./locales/*.json");
    
    export const loadCatalog = async (locale: string) => {
      const load = catalogs[`./locales/${locale}.json`];
      return (await load()) as Record<string, string>;
    };
    

    Ленивая загрузка включена по умолчанию: каждая запись представляет собой функцию, возвращающую динамический импорт, и Rollup генерирует отдельный чанк на каждый файл. Флаг { eager: true }, напротив, встраивает все файлы непосредственно в импортирующий модуль, полностью перечеркивая оптимизацию:

    ts
    // Все языки попадают в начальный чанк. Почти никогда не то, что нужно:
    const catalogs = import.meta.glob("./locales/*.json", { eager: true });
    

    Ловушка в том, что оба варианта работают в dev-режиме, поскольку Vite отдает модули без бандлинга. Разница проявляется только в каталоге dist. Проверьте сборку через npx vite build && npx vite preview и посмотрите, что реально входит во входной чанк.

    Разделение по роутам редко разделяет каталоги на практике

    Это поведение часто застает разработчиков врасплох. Вы раскладываете каталоги по страницам:

    plaintext
    locales/en/home.json
    locales/en/checkout.json
    

    Затем два разных маршрута импортируют checkout.json, и Rollup выносит его в общий разделяемый чанк, который скачивается на обоих маршрутах. Алгоритм чанкинга Rollup опирается на граф модулей, а не на структуру папок: любой модуль, доступный более чем из одной точки входа, становится общим. Добавление третьего маршрута ничего не изменит, а четвертый может перекроить чанки совершенно неожиданно.

    Таким образом, разделение по роутам работает только тогда, когда графы импорта страниц строго изолированы. Если размер бандла критичен, проверяйте его объективно:

    bash
    npx vite build && npx vite-bundle-visualizer
    

    Если нужно жестко зафиксировать границы чанков, воспользуйтесь build.rollupOptions.output.manualChunks, расплачиваясь за это ручной поддержкой конфигурации.

    Каталоги не поддерживают Hot Module Replacement (HMR)

    Меняете компонент — Vite мгновенно обновляет его. Меняете locales/fr.json — и в зависимости от способа импорта ничего не происходит. Динамически импортируемый JSON не имеет встроенной HMR-границы, поэтому граф модулей не знает, как инвалидировать зависимые компоненты.

    Разработчики часто обходят это постоянным перезапуском сервера разработки при каждой правке текста. Решение лежит на стороне i18n-плагина: он обязан перехватывать HMR-обновление и проталкивать свежие сообщения в работающее приложение. Выбирая библиотеку, проверьте, умеет ли ее плагин для Vite обрабатывать HMR для словарей.

    define намертво зашивает локаль в билд

    Возникает соблазн определить локаль по умолчанию на этапе сборки:

    vite.config.ts
    export default defineConfig({
      define: {
        __DEFAULT_LOCALE__: JSON.stringify(process.env.LOCALE ?? "en"),
      },
    });
    

    Конструкция define производит чистую текстовую подстановку во время компиляции. Значение, зашитое при сборке, становится окончательным, что вынуждает делать отдельный билд под каждый язык. Это легитимный подход, именно так устроена нативная интернационализация в Angular, но он не подходит, если единый деплой должен обслуживать все языки одновременно.

    Значения, которые должны варьироваться для каждого запроса, нельзя выносить в define, их необходимо вычислять в рантайме.

    Перенос парсинга сообщений на этап сборки

    Все зрелые решения в экосистеме приходят к одному итогу: перестать парсить сообщения в браузере.

    Плагин Что выносится на этап сборки
    @intlify/unplugin-vue-i18n Компилирует сообщения vue-i18n в render-функции (рантайм без парсера)
    Lingui (макрос + плагин) Извлекает и компилирует каталоги, заменяет макросы на ID сообщений
    Paraglide (inlang) Компилирует каждое сообщение в отдельную tree-shakable функцию
    vite-intlayer Собирает словари компонентов, удаляет и минифицирует неиспользуемое

    Выгода здесь двоякая: из клиентского бандла полностью исчезает компилятор сообщений, а неиспользуемые строки могут быть удалены статически. Сопутствующая цена: и dev-сервер, и CI обязаны использовать плагин, а запуск чистого tsc или тестов вне Vite потребует дополнительной настройки.

    vue-i18n — наглядный пример: без @intlify/unplugin-vue-i18n вы поставляете в продакшен компилятор, дергающий new Function, что ведет к лишнему весу и конфликтам с Content Security Policy (CSP).

    SSR: никогда не храните локаль в переменных модуля

    При использовании SSR (через фреймворк или vite-plugin-ssr) действует строгое правило: переменная на уровне модуля, хранящая текущую локаль, становится общей для всех одновременных запросов в рамках процесса сервера.

    ts
    // Безопасно в браузере. Критическая утечка данных между запросами на сервере:
    export let currentLocale = "en";
    

    Два пользователя, одновременно зашедшие на сервер, попадут в состояние гонки, и один из них получит страницу на языке другого. В локальной разработке это незаметно, так как вы тестируете приложение в одиночку. Всегда определяйте локаль для каждого запроса индивидуально и передавайте ее явно через контекст или request-local хранилище фреймворка.

    Плагин Vite для Intlayer

    Intlayer подключается одним плагином, который берет на себя компиляцию словарей, наблюдение за файлами в dev-режиме и оптимизацию:

    vite.config.ts
    import react from "@vitejs/plugin-react";
    import { defineConfig } from "vite";
    import { intlayer } from "vite-intlayer";
    
    export default defineConfig({
      plugins: [react(), intlayer()],
    });
    

    Перезапись импортов, очистка (purge) и минификация включены по умолчанию. Два главных флага настраиваются в intlayer.config.ts:

    intlayer.config.ts
    import type { IntlayerConfig } from "intlayer";
    
    const config: IntlayerConfig = {
      build: {
        purge: true, // удаляет поля контента, которые не читает ни один компонент
        minify: true, // заменяет длинные ключи контента на короткие псевдонимы
      },
    };
    
    export default config;
    

    Поскольку контент объявляется рядом с компонентами, а не в циклопических файлах локалей, шаг очистки оперирует реальным графом модулей, что делает удаление лишнего кода безопасным. Ограничение то же: плагин обязателен везде, где компилируется код, включая CI и тесты. Подробнее в статье об оптимизации бандла.

    Распространенные ошибки

    • { eager: true } для импорта, задуманного как ленивый. Работает в dev, но тянет все языки в продакшен.
    • Ожидание, что структура папок формирует чанки. Rollup анализирует граф импортов, а не папки. Анализируйте реальный билд.
    • Перезапуск dev-сервера при правках текста. Признак отсутствия поддержки HMR в выбранном решении.
    • Фиксация локали через define. Привязывает вас к отдельной сборке на каждый язык.
    • Хранение локали в модуле при SSR. Приводит к утечке данных между запросами пользователей.
    • Оценка размера бандла по dev-серверу. Несобранные модули не имеют отношения к продакшен-бандлу.

    Полезные материалы

    Комментарии

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

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

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