Библиотека Globalize работает поверх стандарта CLDR и использует набор JSON-данных для локализации:
Полная загрузка всех локалей и всех CLDR-пакетов резко увеличивает размер клиентского JavaScript-приложения. Особенно заметно это в многоязычных интерфейсах, где используются десятки языков и регионов.
Ленивая загрузка (lazy loading) позволяет:
Типичная ошибка — импортировать все данные сразу:
import Globalize fr om "globalize";
import likelySubtags fr om "cldr-data/supplemental/likelySubtags.json";
import numberingSystems fr om "cldr-data/supplemental/numberingSystems.json";
import enNumbers fr om "cldr-data/main/en/numbers.json";
import frNumbers from "cldr-data/main/fr/numbers.json";
import deNumbers from "cldr-data/main/de/numbers.json";
import ruNumbers from "cldr-data/main/ru/numbers.json";
Globalize.load(
likelySubtags,
numberingSystems,
enNumbers,
frNumbers,
deNumbers,
ruNumbers
);
Подход приводит к нескольким проблемам:
| Проблема | Последствие |
|---|---|
| Большой bundle | Медленная загрузка |
| Дублирование данных | Рост памяти |
| Ненужные локали | Лишний сетевой трафик |
| Долгий parse JSON | Увеличение TTI |
| Плохой cache split | Низкая эффективность CDN |
Обычно приложение строится вокруг следующих принципов:
Globalize выполняется после
загрузки CLDR.Схема:
Приложение
↓
Определение locale
↓
Проверка кэша
↓
Динамический import()
↓
Globalize.load()
↓
Создание formatter
↓
Использование locale
Структура проекта:
src/
i18n/
loader.js
cache.js
locales/
en.js
fr.js
ru.js
Supplemental-данные нужны почти всегда:
import Globalize from "globalize";
import likelySubtags from "cldr-data/supplemental/likelySubtags.json";
import plurals from "cldr-data/supplemental/plurals.json";
import numberingSystems from "cldr-data/supplemental/numberingSystems.json";
Globalize.load(
likelySubtags,
plurals,
numberingSystems
);
Эти данные обычно загружаются на старте приложения.
Локализованные данные загружаются отдельно.
Простейшая реализация:
async function loadLocale(locale) {
const data = await import(
`cldr-data/main/${locale}/numbers.json`
);
Globalize.load(data.default);
}
Современные bundler-системы:
автоматически создают отдельные chunks.
Обычно одной секции недостаточно. Для корректной работы нужны:
Пример:
async function loadLocale(locale) {
const modules = await Promise.all([
import(`cldr-data/main/${locale}/numbers.json`),
import(`cldr-data/main/${locale}/currencies.json`),
import(`cldr-data/main/${locale}/ca-gregorian.json`),
import(`cldr-data/main/${locale}/timeZoneNames.json`)
]);
modules.forEach(module => {
Globalize.load(module.default);
});
}
Экземпляр нельзя создавать до загрузки данных.
Неправильно:
const globalize = new Globalize("fr");
await loadLocale("fr");
Правильно:
await loadLocale("fr");
const globalize = new Globalize("fr");
Практический вариант:
import Globalize from "globalize";
const loadedLocales = new Set();
export async function setLocale(locale) {
if (!loadedLocales.has(locale)) {
await loadLocale(locale);
loadedLocales.add(locale);
}
return new Globalize(locale);
}
Даже с кэшем возможна проблема race condition.
Пример:
await Promise.all([
setLocale("de"),
setLocale("de"),
setLocale("de")
]);
Без защиты выполнятся три сетевых запроса.
Решение:
const loadingPromises = new Map();
async function loadLocale(locale) {
if (loadingPromises.has(locale)) {
return loadingPromises.get(locale);
}
const promise = internalLoad(locale);
loadingPromises.set(locale, promise);
await promise;
loadingPromises.delete(locale);
}
async function internalLoad(locale) {
const modules = await Promise.all([
import(`cldr-data/main/${locale}/numbers.json`),
import(`cldr-data/main/${locale}/currencies.json`)
]);
modules.forEach(module => {
Globalize.load(module.default);
});
}
Кроме CLDR часто используются message catalogs.
Структура:
messages/
en.json
fr.json
ru.json
Загрузка:
async function loadMessages(locale) {
const messages = await import(
`./messages/${locale}.json`
);
Globalize.loadMessages({
[locale]: messages.default
});
}
async function initializeLocale(locale) {
await Promise.all([
loadLocale(locale),
loadMessages(locale)
]);
Globalize.locale(locale);
}
Типичная схема:
async function changeLanguage(locale) {
await initializeLocale(locale);
renderApplication();
}
Популярный вариант:
import { useEffect, useState } from "react";
export function useGlobalize(locale) {
const [globalize, setGlobalize] = useState(null);
useEffect(() => {
async function init() {
await initializeLocale(locale);
setGlobalize(new Globalize(locale));
}
init();
}, [locale]);
return globalize;
}
Пока locale не загружена:
function App() {
const globalize = useGlobalize("fr");
if (!globalize) {
return <div>Loading...</div>;
}
return (
<div>
{globalize.formatNumber(1000)}
</div>
);
}
В экосистеме React возможна интеграция с Suspense:
const localePromiseCache = new Map();
function load(locale) {
if (!localePromiseCache.has(locale)) {
localePromiseCache.set(
locale,
initializeLocale(locale)
);
}
throw localePromiseCache.get(locale);
}
Webpack автоматически создаёт chunks:
import(
/* webpackChunkName: "locale-[request]" */
`cldr-data/main/${locale}/numbers.json`
);
Результат:
locale-en.js
locale-fr.js
locale-ru.js
Иногда полезно preload следующего языка.
Например:
loadLocale("en");
сразу после выбора "ru" как fallback.
Если локаль отсутствует:
async function safeLoadLocale(locale) {
try {
await loadLocale(locale);
} catch {
await loadLocale("en");
}
}
Пользовательские locale часто отличаются:
en-US
en-us
EN_us
Нормализация:
function normalizeLocale(locale) {
return locale.toLowerCase();
}
Более корректный вариант:
Intl.getCanonicalLocales(locale)[0];
Определение браузерной локали:
const locale = navigator.language;
Пример:
async function bootstrap() {
const locale = navigator.language;
await initializeLocale(locale);
startApplication();
}
Нельзя доверять любой locale из браузера.
const supportedLocales = [
"en",
"fr",
"de",
"ru"
];
Проверка:
function resolveLocale(locale) {
if (supportedLocales.includes(locale)) {
return locale;
}
return "en";
}
Создание formatter-ов тоже может быть дорогим.
Плохой вариант:
const formatter = globalize.numberFormatter();
при каждом render.
Правильнее:
const formatterCache = new Map();
function getNumberFormatter(locale) {
if (!formatterCache.has(locale)) {
const globalize = new Globalize(locale);
formatterCache.set(
locale,
globalize.numberFormatter()
);
}
return formatterCache.get(locale);
}
const dateFormatterCache = new Map();
function getDateFormatter(locale) {
if (!dateFormatterCache.has(locale)) {
const globalize = new Globalize(locale);
dateFormatterCache.set(
locale,
globalize.dateFormatter({
datetime: "medium"
})
);
}
return dateFormatterCache.get(locale);
}
Для SPA с большим количеством языков память может расти.
Пример LRU-кэша:
class LocaleCache {
constructor(lim it = 5) {
this.lim it = lim it;
this.cache = new Map();
}
set(locale, value) {
if (this.cache.size >= this.lim it) {
const firstKey =
this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(locale, value);
}
get(locale) {
return this.cache.get(locale);
}
}
CLDR-файлы часто выносятся на CDN.
Пример:
async function fetchLocale(locale) {
const response = await fetch(
`/locales/${locale}/numbers.json`
);
return response.json();
}
Последовательный вариант:
await loadNumbers(locale);
await loadCurrencies(locale);
await loadDates(locale);
Неэффективен.
Лучше:
await Promise.all([
loadNumbers(locale),
loadCurrencies(locale),
loadDates(locale)
]);
async function loadJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to load ${url}`
);
}
return response.json();
}
async function retry(fn, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) {
throw error;
}
}
}
}
Использование:
await retry(() => loadLocale("fr"));
Некоторые bundler поддерживают prefetch hints:
import(
/* webpackPrefetch: true */
`./locales/${locale}.json`
);
Браузер загрузит chunk в idle-time.
На сервере стратегия отличается:
Пример:
await initializeLocale(request.locale);
const html = renderToString(app);
<script>
window.__LOCALE__ = "fr";
</script>
Клиент:
await initializeLocale(window.__LOCALE__);
Сервер может встроить locale data:
<script>
window.__CLDR__ = {...};
</script>
Клиент:
Globalize.load(window.__CLDR__);
Модульная архитектура помогает уменьшать bundle.
Например:
import Globalize from "globalize/dist/globalize/number";
Вместо:
import Globalize from "globalize";
Иногда числа и даты нужны не одновременно.
Можно загружать отдельно:
async function loadNumberModule(locale) {
const data = await import(
`cldr-data/main/${locale}/numbers.json`
);
Globalize.load(data.default);
}
И отдельно:
async function loadDateModule(locale) {
const data = await import(
`cldr-data/main/${locale}/ca-gregorian.json`
);
Globalize.load(data.default);
}
Ключевые преимущества:
| Метрика | Улучшение |
|---|---|
| Initial JS | Меньше |
| Parse time | Быстрее |
| Memory usage | Ниже |
| TTI | Лучше |
| CDN cache | Эффективнее |
const formatter =
new Globalize("fr").numberFormatter();
при отсутствии locale data вызовет исключение.
Например:
numbers.json
без:
likelySubtags
numberingSystems
может привести к ошибкам резолвинга.
Без кэширования:
await loadLocale("fr");
будет выполняться многократно.
Плохо:
1 chunk = 1 json
Слишком много запросов.
Лучше:
1 locale = 1 chunk
i18n/
core/
globalize.js
cache.js
manager.js
locales/
en/
cldr.js
messages.js
fr/
cldr.js
messages.js
import Globalize from "globalize";
const loaded = new Set();
const loading = new Map();
async function load(locale) {
if (loaded.has(locale)) {
return;
}
if (loading.has(locale)) {
return loading.get(locale);
}
const promise = (async () => {
const [
cldr,
messages
] = await Promise.all([
import(`./locales/${locale}/cldr.js`),
import(`./locales/${locale}/messages.js`)
]);
Globalize.load(cldr.default);
Globalize.loadMessages({
[locale]: messages.default
});
loaded.add(locale);
})();
loading.set(locale, promise);
await promise;
loading.delete(locale);
}
export async function getGlobalize(locale) {
await load(locale);
return new Globalize(locale);
}
const globalize =
await getGlobalize("ru");
const price =
globalize.formatCurrency(100, "RUB");
console.log(price);
Для SPA удобно связывать locale с URL:
/en/products
/fr/products
/ru/products
Загрузка:
const locale =
location.pathname.split("/")[1];
await initializeLocale(locale);
В архитектуре microfrontend каждый модуль может:
Важно избегать повторного вызова:
Globalize.load(...)
для одинаковых данных.
Полезно измерять:
performance.mark("locale-start");
await initializeLocale("fr");
performance.mark("locale-end");
performance.measure(
"locale-load",
"locale-start",
"locale-end"
);
Иногда locale-файлы дополнительно:
Крупные приложения иногда используют этапы:
Подход особенно полезен при медленном соединении.
В сочетании с Service Worker:
Пример стратегии:
Cache First
для locale chunks.