Динамический импорт переводов

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

Принцип динамической загрузки

Базовая модель i18next предполагает наличие ресурсов перевода в памяти:

i18n.init({
  resources: {
    en: {
      common: {
        welcome: "Welcome"
      }
    }
  },
  lng: "en",
  ns: ["common"],
  defaultNS: "common"
});

Такой подход становится неэффективным при большом количестве языков и пространств имён. Динамический импорт переносит загрузку ресурсов в момент их фактической необходимости.

Ключевые механизмы:

  • разделение переводов по namespaces;
  • загрузка языков по требованию;
  • кэширование загруженных ресурсов;
  • интеграция с bundler-ами (Vite, Webpack, Rollup).

Архитектура namespaces как основа ленивой загрузки

Динамический импорт невозможен без структурирования переводов. Наиболее распространённая модель — разделение по namespace:

locales/
  en/
    common.json
    dashboard.json
  ru/
    common.json
    dashboard.json

Пример содержимого:

// locales/ru/common.json
{
  "welcome": "Добро пожаловать"
}
// locales/ru/dashboard.json
{
  "title": "Панель управления"
}

Инициализация i18next с поддержкой namespace:

i18n.init({
  lng: "ru",
  fallbackLng: "en",
  ns: ["common", "dashboard"],
  defaultNS: "common"
});

Загрузка переводов через i18next-http-backend

Наиболее универсальный способ динамической загрузки — использование backend-модуля:

npm install i18next-http-backend

Конфигурация:

import i18n from "i18next";
import HttpBackend from "i18next-http-backend";

i18n
  .use(HttpBackend)
  .init({
    lng: "ru",
    fallbackLng: "en",
    backend: {
      loadPath: "/locales/{{lng}}/{{ns}}.json"
    }
  });

При таком подходе перевод загружается только при первом обращении к namespace:

i18n.t("dashboard:title");

Если namespace dashboard не загружен, библиотека автоматически выполнит HTTP-запрос:

GET /locales/ru/dashboard.json

Lazy loading namespaces вручную

В некоторых архитектурах требуется явный контроль загрузки переводов.

await i18n.loadNamespaces("dashboard");

i18n.setDefaultNamespace("dashboard");

console.log(i18n.t("title"));

Метод loadNamespaces инициирует подгрузку ресурсов и интегрирует их в текущий instance i18next.


Динамический импорт через ES Modules

При использовании Vite или современных bundler-ов возможен отказ от HTTP-загрузки в пользу import():

async function loadLocale(locale, namespace) {
  const messages = await import(
    `./locales/${locale}/${namespace}.json`
  );

  i18n.addResourceBundle(
    locale,
    namespace,
    messages.default,
    true,
    true
  );
}

Использование:

await loadLocale("ru", "dashboard");

Преимущество такого подхода заключается в:

  • отсутствии сетевых запросов;
  • использовании code splitting на уровне сборщика;
  • предсказуемости загрузки в SPA.

Vite: import.meta.glob для масштабируемых переводов

В Vite-экосистеме динамический импорт переводов часто строится через import.meta.glob:

const locales = import.meta.glob("./locales/**/!.json");

Загрузка конкретного файла:

async function loadLocale(locale, ns) {
  const path = `./locales/${locale}/${ns}.json`;

  const loader = locales[path];
  if (!loader) return;

  const module = await loader();

  i18n.addResourceBundle(locale, ns, module.default);
}

Такой подход позволяет:

  • собрать все переводы в единый реестр;
  • избежать ручного перечисления файлов;
  • использовать tree-shaking на уровне сборки.

Webpack: lazy chunks для переводов

Webpack позволяет вынести переводы в отдельные чанки:

function loadLocale(locale, ns) {
  return import(
    /* webpackChunkName: "i18n-[request]" */
    `./locales/${locale}/${ns}.json`
  ).then((module) => {
    i18n.addResourceBundle(locale, ns, module.default);
  });
}

Каждый язык или namespace превращается в отдельный chunk:

i18n-ru-dashboard.js
i18n-en-common.js

Автоматическая загрузка при смене языка

Механизм смены языка часто связывается с динамической подгрузкой:

async function changeLanguage(lng) {
  await i18n.changeLanguage(lng);

  const namespaces = i18n.options.ns;

  await Promise.all(
    namespaces.map((ns) =>
      i18n.loadNamespaces(ns)
    )
  );
}

В более строгой архитектуре загрузка происходит до смены языка:

async function switchLanguage(lng) {
  const namespaces = ["common", "dashboard"];

  for (const ns of namespaces) {
    await loadLocale(lng, ns);
  }

  await i18n.changeLanguage(lng);
}

Кэширование загруженных переводов

i18next по умолчанию хранит загруженные ресурсы в памяти:

i18n.hasResourceBundle("ru", "dashboard");

Проверка позволяет избежать повторных загрузок:

if (!i18n.hasResourceBundle(lng, ns)) {
  await loadLocale(lng, ns);
}

Для HTTP-backend кэширование может усиливаться через HTTP cache headers:

Cache-Control: public, max-age=31536000, immutable

Ошибки и fallback при динамической загрузке

При отсутствии файла перевода или сетевой ошибке используется fallback language:

i18n.init({
  fallbackLng: "en",
  saveMissing: false
});

Обработка отсутствующих ресурсов:

i18n.on("failedLoading", (lng, ns, msg) => {
  console.error(`Ошибка загрузки ${lng}/${ns}: ${msg}`);
});

Синхронизация с SSR и hydration

В SSR-окружениях динамическая загрузка требует предварительного наполнения ресурсов:

await i18n.init({
  lng: "ru",
  ns: ["common", "dashboard"],
  preload: ["ru"]
});

При гидрации клиент должен получить уже загруженные ресурсы:

i18n.addResourceBundle("ru", "common", serverState.common);

Оптимизация стратегии загрузки

Эффективная схема динамического импорта переводов обычно комбинирует:

  • HTTP backend для масштабируемых приложений;
  • code splitting для критических языков;
  • ручную загрузку namespaces для контроля UX;
  • кеширование ресурсов на уровне i18n и браузера;
  • предзагрузку часто используемых языков.

На практике часто используется гибридная модель:

backend: {
  loadPath: "/locales/{{lng}}/{{ns}}.json",
  allowMultiLoading: true
}

с дополнительным предзагрузчиком:

i18n.loadLanguages(["en", "ru"]);