При использовании библиотеки Globalize одной из главных проблем становится объём CLDR-данных. Полный набор локалей, календарей, валют, правил форматирования чисел и сообщений может занимать сотни килобайт. Если загружать всё сразу, увеличивается:
Особенно критично это для:
Поэтому в реальных проектах применяется динамическая подгрузка локалей.
Обычно приложение:
Globalize после загрузки данных.Простейшая схема:
import Globalize from "globalize";
async function loadLocale(locale) {
const data = await fetch(`/cldr/${locale}.json`)
.then(r => r.json());
Globalize.load(data);
return new Globalize(locale);
}
Теперь локаль загружается только в момент обращения.
Наивный подход:
import "./cldr/en.json";
import "./cldr/fr.json";
import "./cldr/de.json";
import "./cldr/ja.json";
создаёт несколько проблем:
Каждая локаль содержит:
Даже несколько языков способны увеличить бандл на мегабайты.
CLDR-данные должны:
Globalize.load().Чем больше локалей — тем дольше инициализация.
Большинство пользователей используют только одну локаль за сессию.
Для динамической загрузки важно понимать устройство данных.
Обычно используются:
cldr-data/
├── main/
│ ├── en/
│ ├── fr/
│ ├── de/
│ └── ru/
├── supplemental/
Категории:
Содержит локализованные данные:
main/en/numbers.json
main/en/ca-gregorian.json
main/en/currencies.json
Содержит общие правила:
supplemental/likelySubtags.json
supplemental/numberingSystems.json
supplemental/plurals.json
Supplemental-данные обычно загружаются один раз при старте приложения.
Хорошая практика — делить данные на:
Загружаются сразу:
Globalize.load(
likelySubtags,
plurals,
numberingSystems
);
Подгружаются по требованию:
async function loadLocaleData(locale) {
const files = await Promise.all([
import(`./cldr/main/${locale}/numbers.json`),
import(`./cldr/main/${locale}/currencies.json`),
import(`./cldr/main/${locale}/ca-gregorian.json`)
]);
files.forEach(file => {
Globalize.load(file.default);
});
}
Современный стандартный способ — динамический импорт.
async function setLocale(locale) {
const messages = await import(
`./messages/${locale}.json`
);
const numbers = await import(
`./cldr/main/${locale}/numbers.json`
);
Globalize.load(numbers.default);
Globalize.loadMessages({
[locale]: messages.default
});
Globalize.locale(locale);
}
Преимущества:
В Webpack динамические импорты автоматически создают чанки.
async function loadMessages(locale) {
return import(
/* webpackChunkName: "locale-[request]" */
`./messages/${locale}.json`
);
}
Будут созданы файлы:
locale-en.js
locale-fr.js
locale-ru.js
В Vite удобно использовать import.meta.glob.
const localeFiles = import.meta.glob(
"./messages/*.json"
);
async function loadMessages(locale) {
const loader = localeFiles[
`./messages/${locale}.json`
];
const module = await loader();
return module.default;
}
После появления lazy loading вся инициализация становится асинхронной.
Globalize.locale("fr");
const formatter = Globalize.numberFormatter();
Если данные ещё не загружены — возникнет ошибка.
async function initLocale(locale) {
await loadLocaleData(locale);
Globalize.locale(locale);
return {
number: Globalize.numberFormatter(),
date: Globalize.dateFormatter()
};
}
Без кэша локали могут загружаться повторно.
const loadedLocales = new Set();
async function ensureLocale(locale) {
if (loadedLocales.has(locale)) {
return;
}
await loadLocaleData(locale);
loadedLocales.add(locale);
}
Теперь повторная загрузка исключается.
Создание formatter-объектов в Globalize достаточно дорогое.
function formatPrice(value) {
return Globalize
.currencyFormatter("USD")(value);
}
Formatter создаётся при каждом вызове.
const formatterCache = new Map();
function getCurrencyFormatter(locale) {
const key = `${locale}-USD`;
if (!formatterCache.has(key)) {
Globalize.locale(locale);
formatterCache.set(
key,
Globalize.currencyFormatter("USD")
);
}
return formatterCache.get(key);
}
Иногда выгодно заранее подгружать вероятные языки.
if (navigator.language.startsWith("fr")) {
preloadLocale("fr");
}
<link rel="prefetch">Можно заранее подсказать браузеру будущую загрузку.
<link
rel="prefetch"
href="/locales/fr.json"
/>
Браузер загрузит файл в idle-время.
Чаще всего используется:
const locale = navigator.language;
Например:
en-US
fr-FR
ru-RU
CLDR может использовать сокращённые коды.
function normalizeLocale(locale) {
return locale.split("-")[0];
}
Результат:
en-US → en
fr-CA → fr
ru-RU → ru
Не все локали могут существовать.
const supported = ["en", "fr", "ru"];
function resolveLocale(locale) {
const normalized =
normalizeLocale(locale);
if (supported.includes(normalized)) {
return normalized;
}
return "en";
}
Обычно вместе с CLDR загружаются message catalog-файлы.
messages/
├── en.json
├── fr.json
└── ru.json
async function loadMessages(locale) {
const messages = await import(
`./messages/${locale}.json`
);
Globalize.loadMessages({
[locale]: messages.default
});
}
import Globalize from "globalize";
const loaded = new Set();
async function setupLocale(locale) {
locale = resolveLocale(locale);
if (!loaded.has(locale)) {
const [
numbers,
calendars,
currencies,
messages
] = await Promise.all([
import(`./cldr/${locale}/numbers.json`),
import(`./cldr/${locale}/ca-gregorian.json`),
import(`./cldr/${locale}/currencies.json`),
import(`./messages/${locale}.json`)
]);
Globalize.load(
numbers.default,
calendars.default,
currencies.default
);
Globalize.loadMessages({
[locale]: messages.default
});
loaded.add(locale);
}
Globalize.locale(locale);
}
Сеть может быть недоступна.
async function safeLoadLocale(locale) {
try {
await setupLocale(locale);
} catch (error) {
console.error(
"Ошибка загрузки локали",
error
);
await setupLocale("en");
}
}
Пока локаль загружается, интерфейс может показывать placeholder.
async function changeLanguage(locale) {
showLoader();
await setupLocale(locale);
hideLoader();
renderApp();
}
Одно из преимуществ lazy loading — возможность менять язык без перезагрузки страницы.
languageSelect.addEventListener(
"change",
async event => {
const locale = event.target.value;
await setupLocale(locale);
rerender();
}
);
В React локали часто загружаются через context.
const LocaleContext =
React.createContext();
function LocaleProvider({ children }) {
const [locale, setLocale] =
useState("en");
async function changeLocale(next) {
await setupLocale(next);
setLocale(next);
}
return (
<LocaleContext.Provider
value={{
locale,
changeLocale
}}
>
{children}
</LocaleContext.Provider>
);
}
В Vue.js используется похожий подход.
const locale = ref("en");
async function setLocale(next) {
await setupLocale(next);
locale.value = next;
}
В server-side rendering важно:
await setupLocale(locale);
const html = renderToString(app);
await hydrateLocale(window.__LOCALE__);
Проблема:
changeLanguage("fr");
changeLanguage("de");
Если fr загрузится позже de, интерфейс
может перейти обратно на французский.
let requestId = 0;
async function changeLanguage(locale) {
const id = ++requestId;
await setupLocale(locale);
if (id !== requestId) {
return;
}
render();
}
Для HTTP-запросов можно отменять старые загрузки.
let controller;
async function fetchLocale(url) {
if (controller) {
controller.abort();
}
controller = new AbortController();
const response = await fetch(url, {
signal: controller.signal
});
return response.json();
}
Хорошая практика — создавать отдельные чанки:
locale-en.js
locale-fr.js
locale-ru.js
а не единый:
locales.js
Это уменьшает сетевой трафик.
Некоторые bundler’ы могут исключать неиспользуемые данные.
Однако CLDR JSON-файлы обычно считаются side-effect ресурсами и редко эффективно tree-shake’ятся. Поэтому lazy loading остаётся главным методом оптимизации.
Локали можно хранить отдельно от основного приложения.
const base =
"https://cdn.example.com/cldr";
const response = await fetch(
`${base}/${locale}/numbers.json`
);
Преимущества:
Для locale-файлов полезны:
Cache-Control: public, max-age=31536000
и versioned URLs:
/locales/v3/fr.json
Для больших enterprise-приложений локали могут храниться в IndexedDB.
Это особенно полезно для PWA.
В экосистеме Globalize Compiler существует подход с предкомпиляцией.
Вместо:
Globalize.numberFormatter()
во время runtime,
форматтер генерируется заранее.
Преимущества:
Часто локали загружаются вместе с route chunks.
const AdminPage = lazy(async () => {
await setupLocale(currentLocale);
return import("./AdminPage");
});
Некоторые старые архитектуры ожидают:
translate("hello");
без ожидания загрузки.
После внедрения lazy loading требуется:
<Suspense fallback={<Spinner />}>
<App />
</Suspense>
Локаль может загружаться через asynchronous boundary.
Обычно локаль хранится:
localStorage.setItem(
"locale",
"fr"
);
const locale =
localStorage.getItem("locale")
|| "en";
Пример:
/en/products
/fr/products
/ru/products
Локаль извлекается из URL:
const locale =
location.pathname.split("/")[1];
В microfrontend-архитектуре каждый модуль может иметь собственные locale chunks.
Важно избегать:
Globalize.load();Крупные приложения часто создают единый сервис.
localeManager.load("fr");
localeManager.set("fr");
localeManager.formatNumber(1000);
Это упрощает:
Globalize.locale("fr");
без Globalize.load().
Ошибка:
E_MISSING_CLDR
Globalize.load(data);
при каждом рендере.
setupLocale("uk-UA");
при отсутствии данных.
const text = translate("hello");
до завершения загрузки локали.
Часто используется следующая структура:
src/
├── i18n/
│ ├── localeManager.js
│ ├── loaders/
│ ├── cache/
│ ├── formatters/
│ └── messages/
├── cldr/
└── app/
Для production-систем оптимальной считается комбинация:
Такой подход обеспечивает: