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

    Vite i18n: аспекти, що стосуються саме Vite, а не вашого фреймворку

    Більшість посібників із назвою "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";
    

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

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

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

    Поділ за маршрутами рідко розділяє каталоги на практиці

    Ця поведінка часто дивує розробників. Ви структуруєте каталоги за сторінками:

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

    Потім два різних маршрути імпортують checkout.json, і Rollup виносить цей файл у спільний чанк (shared chunk), який завантажується на обох сторінках. Механізм чанкінгу Rollup керується графом залежностей модулів, а не назвами ваших папок: будь-який модуль, доступний із більш ніж однієї точки входу, стає спільним. Додавання третього маршруту нічого не змінить, а четвертий може взагалі несподівано перекроїти структуру чанків.

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

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

    Якщо вам конче необхідно зафіксувати межі чанків, опція build.rollupOptions.output.manualChunks є єдиним виходом, але ціною постійного ручного супроводу.

    Каталоги не підтримують гаряче перезавантаження (HMR) автоматично

    Змініть компонент — і Vite миттєво оновить його на екрані. Змініть locales/fr.json — і залежно від способу імпорту нічого не станеться. Динамічно імпортований JSON не має власної межі HMR, тому граф модулів не знає, як саме слід інвалідувати залежні компоненти.

    Розробники зазвичай обходять це перезапуском dev-сервера щоразу, коли змінюють текст. Правильне вирішення проблеми лежить на боці плагіна i18n: він повинен перехоплювати HMR-оновлення та передавати нові повідомлення в працюючу програму. Обираючи бібліотеку, перевірте, чи вміє її плагін для Vite обробляти HMR для словників.

    define намертво фіксує мову в зібраному коді

    Виникає спокуса зафіксувати типову локаль під час компіляції:

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

    Опція define виконує пряму текстову заміну на етапі збірки. Значення, зашите під час компіляції, стає остаточним, що змушує збирати окремий білд для кожної мови. Це робоча стратегія (саме так реалізована офіційна i18n в Angular), але це зовсім не те, що вам потрібно, якщо одне розгортання має обслуговувати всі мови одночасно.

    Значення, які повинні змінюватися залежно від запиту користувача, не слід зашивати в define — їх треба розв'язувати під час виконання (runtime).

    Перенесення парсингу повідомлень на етап збірки

    Усі зрілі рішення в екосистемі зрештою приходять до одного висновку: припинити парсити повідомлення в браузері клієнта.

    Плагін Що переноситься на етап збірки
    @intlify/unplugin-vue-i18n Компілює повідомлення vue-i18n у функції рендерингу (рантайм-бандл)
    Lingui (макрос + плагін) Витягує та компілює каталоги, замінює макроси на ідентифікатори
    Paraglide (inlang) Компілює кожне повідомлення в окрему tree-shakable функцію
    vite-intlayer Будує словники компонентів, очищає (purge) та мініфікує невідоме

    Вигода подвійна: важкий компілятор повідомлень більше не потрапляє в клієнтський бандл, а невикористані фрази видаляються статично. Плата за це: і dev-сервер, і CI повинні запускати плагін, а для запуску простого tsc або тестів поза Vite знадобиться додаткова конфігурація.

    SSR: ніколи не зберігайте локаль у стані модуля

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

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

    Двоє користувачів, які одночасно звертаються до сервера, потраплять у стан перегонів (race condition), і один із них побачить сторінку мовою іншого. Під час локальної розробки це непомітно, оскільки ви єдиний тестувальник. Завжди визначайте локаль для кожного запиту окремо і передавайте її явно через контекст або request-local сховище фреймворку.

    Плагін Vite для Intlayer

    Intlayer надає єдиний плагін, який бере на себе збірку словників, спостереження за файлами в режимі розробки та конвеєр оптимізації:

    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;
    

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

    Поширені помилки

    • { eager: true } для glob, який мав завантажуватися ліниво. Працює локально, але тягне всі мови в продакшен.
    • Очікування, що структура папок автоматично сформує чанки. Rollup орієнтується на імпорти, а не на папки.
    • Перезапуск dev-сервера для перегляду змін у тексті. Свідчення відсутності обробника HMR у плагіні.
    • Вшивання локалі в define. Прив'язує проєкт до окремої збірки на кожну мову.
    • Збереження стану локалі на рівні модуля в SSR. Призводить до змішування мов між паралельними запитами.
    • Вимірювання продуктивності бандла на dev-сервері. Незібрані окремі модулі не відображають структуру продакшен-бандла.

    Корисні матеріали

    Коментарі

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

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

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