Code splitting по локалям

Библиотека Globalize строится поверх данных CLDR (Common Locale Data Repository), которые содержат огромный набор информации: форматы дат, чисел, валют, правила плюрализации, временные зоны и языковые особенности. Полный набор CLDR для всех локалей измеряется мегабайтами данных, что делает их загрузку целиком на старте приложения неэффективной.

Основная проблема без разделения:

  • загрузка всех локалей увеличивает initial bundle;
  • парсинг CLDR замедляет старт приложения;
  • большинство пользователей используют одну локаль;
  • динамическое переключение локалей требует повторной инициализации Globalize.

Code splitting по локалям решает задачу изоляции языковых данных в отдельные чанки, загружаемые только при необходимости.


Структура данных Globalize и влияние на размер бандла

Globalize не работает «из коробки» без внешних данных. Для каждой локали требуется загрузка:

  • CLDR supplemental data (минимальный общий набор);
  • locale-specific numbers;
  • locale-specific dates;
  • messages (если используются переводы);
  • currency formats (при необходимости).

Типичная ошибка — импортировать все локали сразу:

import Globalize from "globalize";

import "cldr-data";
import "cldr-data/main/ru";
import "cldr-data/main/en";
import "cldr-data/main/ar";

Такой подход приводит к росту начального бандла и потере преимуществ tree-shaking.


Принцип code splitting локалей

Code splitting в контексте Globalize строится на идее:

  • базовый бандл содержит только Globalize и минимальный CLDR supplemental;
  • каждая локаль выносится в отдельный динамический модуль;
  • загрузка локали происходит лениво, по требованию;
  • после загрузки выполняется Globalize.load() и Globalize.locale().

Ключевой момент: CLDR данные не являются автоматически ленивыми, их необходимо явно разделять на модули.


Базовая конфигурация: минимальный runtime

Инициализация общего слоя:

import Globalize from "globalize";

import cldrSupplemental from "cldr-data/supplemental/likelySubtags.json";
import cldrWeekData from "cldr-data/supplemental/weekData.json";
import cldrNumberingSystems from "cldr-data/supplemental/numberingSystems.json";

Globalize.load(cldrSupplemental, cldrWeekData, cldrNumberingSystems);

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


Организация локалей через динамические импорты

Каждая локаль выносится в отдельный модуль.

Структура файлов

locales/
  ru.js
  en.js
  ar.js

Пример локального модуля

// locales/ru.js
import Globalize from "globalize";

import cldrNumbers from "cldr-data/main/ru/numbers.json";
import cldrDates from "cldr-data/main/ru/ca-gregorian.json";
import cldrTimeData from "cldr-data/main/ru/timeZoneNames.json";
import messages from "./messages/ru.json";

Globalize.load(cldrNumbers, cldrDates, cldrTimeData);
Globalize.loadMessages(messages);

export function activateRu() {
  Globalize.locale("ru");
}

Каждая локаль содержит только свои данные, что позволяет эффективно разделять код.


Динамическая загрузка локали

Webpack / Vite / Rollup подход

export async function loadLocale(locale) {
  switch (locale) {
    case "ru": {
      const mod = await import("./locales/ru.js");
      mod.activateRu();
      return;
    }

    case "en": {
      const mod = await import("./locales/en.js");
      mod.activateEn();
      return;
    }

    case "ar": {
      const mod = await import("./locales/ar.js");
      mod.activateAr();
      return;
    }

    default:
      throw new Error("Unsupported locale");
  }
}

Каждый import() создаёт отдельный chunk, который загружается только при первом обращении.


Фабрика локалей для масштабируемой архитектуры

При большом количестве языков switch-case становится непрактичным. Используется маппинг:

const loaders = {
  ru: () => import("./locales/ru.js"),
  en: () => import("./locales/en.js"),
  ar: () => import("./locales/ar.js"),
};

export async function setLocale(locale) {
  const loader = loaders[locale];

  if (!loader) {
    throw new Error(`Locale ${locale} not found`);
  }

  const mod = await loader();
  mod.activate();
}

Инкапсуляция загрузки CLDR внутри локали

Более строгий вариант — каждая локаль сама загружает CLDR:

// locales/en.js
import Globalize from "globalize";

import numbers from "cldr-data/main/en/numbers.json";
import dates from "cldr-data/main/en/ca-gregorian.json";
import timeZoneNames from "cldr-data/main/en/timeZoneNames.json";
import messages from "./messages/en.json";

export function activate() {
  Globalize.load(numbers, dates, timeZoneNames);
  Globalize.loadMessages(messages);
  Globalize.locale("en");
}

Такой подход упрощает поддержку, но требует аккуратного контроля дублирования данных.


Оптимизация повторной загрузки локалей

Без кеширования динамические импорты могут приводить к повторной загрузке модулей в некоторых runtime-сценариях (особенно при SPA-навигации).

Используется простой кеш:

const cache = new Map();

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

  const mod = await import(`./locales/${locale}.js`);
  cache.set(locale, mod.activate);

  mod.activate();
}

Кэш хранит не данные CLDR, а функцию активации, что минимизирует память.


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

В Globalize важно разделять:

  • CLDR данные (форматы);
  • messages (переводы интерфейса).

Сообщения часто обновляются чаще, чем CLDR, поэтому их удобно хранить отдельно:

Globalize.loadMessages({
  "ru": {
    hello: "Привет",
    date_format: "{0}"
  }
});

При code splitting сообщения также выносятся:

export async function loadMessages(locale) {
  const messages = await import(`./messages/${locale}.json`);
  Globalize.loadMessages(messages.default);
}

Связка с состоянием приложения

Обычно локаль хранится в глобальном state:

let currentLocale = "en";

export async function changeLocale(locale) {
  await setLocale(locale);
  currentLocale = locale;
}

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


Производительность и влияние на bundle size

Разделение локалей даёт следующие эффекты:

  • уменьшение initial JS bundle;
  • ускорение first contentful paint;
  • возможность кеширования локалей браузером отдельно;
  • снижение памяти на клиенте.

Типичная разница:

  • без code splitting: +2–10 MB CLDR данных;
  • с code splitting: +20–150 KB на локаль при загрузке по требованию.

SSR и предварительная загрузка локалей

При серверном рендеринге локаль должна быть известна заранее:

export function createGlobalize(locale) {
  const Globalize = require("globalize");

  require(`cldr-data/main/${locale}/numbers.json`);
  require(`cldr-data/main/${locale}/ca-gregorian.json`);

  Globalize.locale(locale);

  return Globalize;
}

SSR-стратегия обычно предполагает отсутствие динамического import(), так как все данные должны быть доступны синхронно.


Стратегии предзагрузки

Для улучшения UX применяются гибридные подходы:

  • предзагрузка локали пользователя по Accept-Language;
  • lazy loading вторичных языков;
  • prefetch соседних локалей при idle time.
if (navigator.language.startsWith("ru")) {
  import("./locales/ru.js");
}

Ошибки архитектуры при code splitting

Типичные проблемы:

  • загрузка CLDR вне локального модуля, что разрушает изоляцию;
  • отсутствие supplemental data в базовом бандле;
  • дублирование CLDR между локалями;
  • повторный Globalize.load() без контроля;
  • отсутствие кеширования динамических импортов.

Масштабирование на десятки локалей

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

const locales = ["en", "ru", "ar", "fr", "de", "zh"];

const loaders = Object.fromEntries(
  locales.map(l => [l, () => import(`./locales/${l}.js`)])
);

Такой подход хорошо сочетается с автоматической генерацией через build-скрипты.