Международные приложения редко используют все локали одновременно. Обычно активна одна языковая конфигурация, а остальные представляют собой потенциальный, но не загруженный набор данных. Это делает стратегию ленивой загрузки ключевым механизмом оптимизации:
Локализация в 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, позволяя
загрузить перевод только при фактической необходимости.
В экосистеме 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();Часто локаль загружается одновременно с маршрутом приложения. Это позволяет синхронизировать 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,
};
},
},
];
Такой подход снижает вероятность ситуации, при которой интерфейс отображается без перевода.
Ленивая загрузка компонентов может быть связана с локалями напрямую:
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]();
}
}
Предзагрузка может выполняться:
Повторная загрузка одной и той же локали недопустима с точки зрения производительности. Поэтому применяется кэш-слой:
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 автоматически выделяет динамические импорты в отдельные чанки:
import(`./locales/${locale}/messages`);
Каждый язык становится отдельным файлом:
ru.[hash].jsen.[hash].jsde.[hash].jsЭто позволяет CDN доставлять только необходимый контент.
FormatJS использует ICU MessageFormat. При ленивой загрузке важно, чтобы структура сообщений оставалась консистентной между локалями:
{
items: "Элементов: {count, plural, one {# элемент} few {# элемента} many {# элементов} other {# элементов}}"
}
Ленивая загрузка не влияет на синтаксис ICU, но требует строгого соблюдения согласованности ключей между файлами.
На практике встречаются типичные проблемы:
1. Отсутствие fallback-локали
Если динамический импорт не возвращает модуль, интерфейс остаётся без текста.
2. Несинхронизированные ключи
Разные локали содержат различный набор ключей, что приводит к
runtime-ошибкам в react-intl.
3. Дублирование локалей в бандле
Статический импорт локалей одновременно с динамическим ломает код-сплиттинг.
Для стабильной архитектуры применяются следующие подходы:
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.