Ленивая загрузка локалей

Принцип ленивой загрузки в международных приложениях

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

  • сокращение начального бандла;
  • уменьшение времени первого рендера;
  • распределение загрузки переводов по мере необходимости;
  • контроль над потреблением памяти и сети.

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


Архитектурная модель локалей

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

/locales
  /en
    messages.js
    date-fns.js
    numbers.js
  /ru
    messages.js
    date-fns.js
    numbers.js
  /de
    messages.js
    date-fns.js
    numbers.js

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


Базовая идея динамической подгрузки

Ленивая загрузка локалей реализуется через динамический импорт ES Modules:

export async function loadLocaleData(locale) {
  switch (locale) {
    case 'ru':
      return import('./locales/ru/messages');
    case 'en':
      return import('./locales/en/messages');
    case 'de':
      return import('./locales/de/messages');
    default:
      return import('./locales/en/messages');
  }
}

Динамический import() возвращает Promise, позволяя загрузить перевод только при фактической необходимости.


Интеграция с IntlProvider

В экосистеме FormatJS основная точка применения локалей — IntlProvider из react-intl. Он принимает набор сообщений и текущую локаль.

import { IntlProvider } from 'react-intl';
import { useEffect, useState } from 'react';
import { loadLocaleData } from './loadLocaleData';

function App({ locale }) {
  const [messages, setMessages] = useState(null);

  useEffect(() => {
    let isActive = true;

    loadLocaleData(locale).then((module) => {
      if (isActive) {
        setMessages(module.default);
      }
    });

    return () => {
      isActive = false;
    };
  }, [locale]);

  if (!messages) {
    return null;
  }

  return (
    <IntlProvider locale={locale} messages={messages}>
      {/* приложение */}
    </IntlProvider>
  );
}

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


Разделение сообщений и кода

FormatJS предполагает разделение runtime и translation payload. Сообщения представляют собой JSON-подобные структуры:

// locales/ru/messages.js
export default {
  greeting: "Привет, {name}",
  unread: "Непрочитанные сообщения: {count}",
};

При ленивой загрузке важно, чтобы сообщения не попадали в основной бандл. Это достигается через:

  • динамический import();
  • отдельные чанки сборщика (Webpack, Vite, Rollup);
  • отключение статических импортов локалей.

Код-сплиттинг на уровне маршрутов

Часто локаль загружается одновременно с маршрутом приложения. Это позволяет синхронизировать UI и переводы.

const routes = [
  {
    path: '/dashboard',
    load: async () => {
      const [messages, component] = await Promise.all([
        import('./locales/ru/messages'),
        import('./pages/Dashboard'),
      ]);

      return {
        component: component.default,
        messages: messages.default,
      };
    },
  },
];

Такой подход снижает вероятность ситуации, при которой интерфейс отображается без перевода.


Использование React.lazy и локалей

Ленивая загрузка компонентов может быть связана с локалями напрямую:

import { Suspense, lazy } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));

function AppWrapper({ messages, locale }) {
  return (
    <Suspense fallback={null}>
      <Dashboard locale={locale} messages={messages} />
    </Suspense>
  );
}

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


Предзагрузка локалей

Хотя модель ленивой загрузки подразумевает реактивную подгрузку, в ряде случаев применяется предзагрузка:

const localePromises = {
  en: () => import('./locales/en/messages'),
  ru: () => import('./locales/ru/messages'),
  de: () => import('./locales/de/messages'),
};

export function preloadLocale(locale) {
  if (localePromises[locale]) {
    return localePromises[locale]();
  }
}

Предзагрузка может выполняться:

  • при наведении на переключатель языка;
  • после первого рендера;
  • в фоне через idle callbacks.

Кэширование загруженных локалей

Повторная загрузка одной и той же локали недопустима с точки зрения производительности. Поэтому применяется кэш-слой:

const cache = new Map();

export async function getLocaleMessages(locale) {
  if (cache.has(locale)) {
    return cache.get(locale);
  }

  const messages = await import(`./locales/${locale}/messages`);
  cache.set(locale, messages.default);

  return messages.default;
}

Кэширование устраняет лишние сетевые запросы и снижает задержки переключения языка.


Работа с webpack и чанками

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

import(`./locales/${locale}/messages`);

Каждый язык становится отдельным файлом:

  • ru.[hash].js
  • en.[hash].js
  • de.[hash].js

Это позволяет CDN доставлять только необходимый контент.


Совместимость с ICU-форматами

FormatJS использует ICU MessageFormat. При ленивой загрузке важно, чтобы структура сообщений оставалась консистентной между локалями:

{
  items: "Элементов: {count, plural, one {# элемент} few {# элемента} many {# элементов} other {# элементов}}"
}

Ленивая загрузка не влияет на синтаксис ICU, но требует строгого соблюдения согласованности ключей между файлами.


Ошибки при ленивой загрузке локалей

На практике встречаются типичные проблемы:

1. Отсутствие fallback-локали

Если динамический импорт не возвращает модуль, интерфейс остаётся без текста.

2. Несинхронизированные ключи

Разные локали содержат различный набор ключей, что приводит к runtime-ошибкам в react-intl.

3. Дублирование локалей в бандле

Статический импорт локалей одновременно с динамическим ломает код-сплиттинг.


Стратегии устойчивой загрузки

Для стабильной архитектуры применяются следующие подходы:

  • единый индекс локалей;
  • строгая типизация сообщений (TypeScript или JSON schema);
  • автоматическая проверка ключей между языками;
  • централизованный loader;
  • fallback на базовую локаль.
const DEFAULT_LOCALE = 'en';

export async function safeLoadLocale(locale) {
  try {
    return await getLocaleMessages(locale);
  } catch {
    return await getLocaleMessages(DEFAULT_LOCALE);
  }
}

Связь с форматированием дат и чисел

Ленивая загрузка затрагивает не только текстовые сообщения, но и локализованные правила форматирования:

  • Intl.DateTimeFormat;
  • Intl.NumberFormat;
  • Intl.RelativeTimeFormat.

FormatJS позволяет подгружать дополнительные polyfills и CLDR-данные по мере необходимости, что дополнительно снижает размер начальной загрузки приложения.


Оптимизация пользовательского опыта

Ленивая загрузка локалей влияет на несколько аспектов интерфейса:

  • сокращение времени до интерактивности;
  • уменьшение блокировки главного потока;
  • плавное переключение языка при наличии кэширования;
  • возможность частичной загрузки интерфейса без полного набора переводов.

Основной эффект достигается за счёт разделения translation layer и application core.