При интернационализации интерфейсов значительная часть ресурсов загружается ещё до того, как пользователь действительно начинает взаимодействовать с приложением. В крупных проектах это приводит к нескольким проблемам:
В экосистеме FormatJS ленивая инициализация позволяет создавать объекты интернационализации только в момент фактического использования.
Наиболее часто лениво инициализируются:
IntlShape;Стандартная инициализация:
import {createIntl, createIntlCache} from '@formatjs/intl'
const cache = createIntlCache()
const intl = createIntl({
locale: 'ru',
messages: messagesRu
}, cache)
Объект intl создаётся сразу после загрузки модуля.
Ленивая инициализация переносит создание объекта в момент обращения:
import {createIntl, createIntlCache} from '@formatjs/intl'
const cache = createIntlCache()
let intl = null
function getIntl() {
if (!intl) {
intl = createIntl({
locale: 'ru',
messages: messagesRu
}, cache)
}
return intl
}
Теперь объект будет создан только при первом вызове
getIntl().
На практике наиболее распространён подход с singleton-экземпляром.
import {createIntl, createIntlCache} from '@formatjs/intl'
const cache = createIntlCache()
let instance
export function getIntl(locale, messages) {
if (!instance) {
instance = createIntl(
{
locale,
messages
},
cache
)
}
return instance
}
Одна из главных причин использования lazy initialization — динамическая загрузка message catalog.
import ruMessages from './locales/ru.json'
import enMessages from './locales/en.json'
Недостаток очевиден: оба файла попадают в основной bundle.
Современный подход:
async function loadMessages(locale) {
const messages = await import(
`./locales/${locale}.json`
)
return messages.default
}
Теперь webpack, Vite или Rollup создают отдельные chunks.
После динамической загрузки сообщений объект intl
создаётся асинхронно.
import {createIntl, createIntlCache} from '@formatjs/intl'
const cache = createIntlCache()
let intl
export async function initIntl(locale) {
const messages = await loadMessages(locale)
intl = createIntl(
{
locale,
messages
},
cache
)
return intl
}
Использование:
await initIntl('ru')
При переключении языков повторная загрузка переводов создаёт ненужные сетевые запросы.
import {createIntl, createIntlCache} from '@formatjs/intl'
const cache = createIntlCache()
const intlMap = new Map()
export async function getIntl(locale) {
if (intlMap.has(locale)) {
return intlMap.get(locale)
}
const messages = await loadMessages(locale)
const intl = createIntl(
{
locale,
messages
},
cache
)
intlMap.set(locale, intl)
return intl
}
В React-приложениях библиотека чаще всего используется через
IntlProvider.
Стандартный вариант:
<IntlProvider locale="ru" messages={messages}>
<App />
</IntlProvider>
При таком подходе сообщения должны быть загружены заранее.
Пример ленивой загрузки локалей:
import {useEffect, useState} from 'react'
import {IntlProvider} from 'react-intl'
export function I18nProvider({locale, children}) {
const [messages, setMessages] = useState(null)
useEffect(() => {
import(`./locales/${locale}.json`)
.then(module => {
setMessages(module.default)
})
}, [locale])
if (!messages) {
return <div>Loading...</div>
}
return (
<IntlProvider
locale={locale}
messages={messages}
>
{children}
</IntlProvider>
)
}
В современных React-приложениях lazy initialization часто комбинируется с Suspense.
const messagesPromiseMap = new Map()
function loadMessages(locale) {
if (!messagesPromiseMap.has(locale)) {
messagesPromiseMap.set(
locale,
import(`./locales/${locale}.json`)
)
}
return messagesPromiseMap.get(locale)
}
Компонент:
function LocaleLoader({locale, children}) {
const messages = use(loadMessages(locale))
return (
<IntlProvider
locale={locale}
messages={messages.default}
>
{children}
</IntlProvider>
)
}
Некоторые API Intl отсутствуют в старых браузерах:
Intl.RelativeTimeFormat;Intl.DisplayNames;Intl.ListFormat;Intl.PluralRules.Подключение всех polyfill заранее резко увеличивает размер bundle.
async function ensurePluralRules(locale) {
if (!Intl.PluralRules) {
await import('@formatjs/intl-pluralrules/polyfill')
}
await import(
`@formatjs/intl-pluralrules/locale-data/${locale}`
)
}
Создание formatter-объектов — дорогостоящая операция.
Например:
new Intl.DateTimeFormat('ru')
или:
new Intl.NumberFormat('en')
FormatJS использует внутренние механизмы кэширования.
import {createIntlCache} from '@formatjs/intl'
const cache = createIntlCache()
Кэш содержит:
DateTimeFormat;NumberFormat;PluralRules;RelativeTimeFormat.Без кэша formatter-объекты создаются заново:
function formatPrice(value) {
return new Intl.NumberFormat('ru').format(value)
}
С кэшем:
const formatter = new Intl.NumberFormat('ru')
function formatPrice(value) {
return formatter.format(value)
}
Разница особенно заметна:
Иногда переводчики нужны не сразу после запуска приложения.
Например:
В таких случаях инициализация переносится до первого открытия модуля.
let adminIntl = null
export async function getAdminIntl() {
if (!adminIntl) {
const messages = await import(
'./admin/messages/ru.json'
)
adminIntl = createIntl({
locale: 'ru',
messages: messages.default
})
}
return adminIntl
}
Крупные приложения часто разбивают переводы на namespace.
Структура:
locales/
ru/
common.json
dashboard.json
profile.json
async function loadNamespace(locale, namespace) {
const module = await import(
`./locales/${locale}/${namespace}.json`
)
return module.default
}
Ленивая инициализация не исключает prefetching.
Например, после авторизации можно заранее загрузить вероятную локаль:
import(
/* webpackPrefetch: true */
'./locales/ru.json'
)
| Механизм | Назначение |
|---|---|
| preload | нужен немедленно |
| prefetch | понадобится позже |
Server-Side Rendering требует осторожности.
Ошибка:
let intl
В Node.js singleton будет общим для всех запросов.
export async function createRequestIntl(locale) {
const messages = await loadMessages(locale)
return createIntl({
locale,
messages
})
}
Каждый HTTP-запрос получает собственный экземпляр.
Иначе возможны:
При гидратации React-приложения можно откладывать загрузку редких локалей.
Пример:
const AdminPanel = lazy(() => import('./AdminPanel'))
Внутри:
const messages = await import(
'./locales/admin/ru.json'
)
Даже при lazy initialization повторное создание formatter-объектов остаётся проблемой.
const formatterMap = new Map()
function getCurrencyFormatter(locale) {
if (!formatterMap.has(locale)) {
formatterMap.set(
locale,
new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
})
)
}
return formatterMap.get(locale)
}
const Price = React.memo(function Price({
value,
formatter
}) {
return formatter.format(value)
})
Formatter создаётся один раз и переиспользуется компонентами.
Частая ошибка — полная повторная инициализация приложения.
Неэффективно:
window.location.reload()
Лучше:
const intl = await getIntl(locale)
setIntl(intl)
Иногда переводы обновляются динамически.
Подход:
Пример:
async function getMessages(locale) {
const cached = cache.get(locale)
fetchNewMessages(locale)
return cached
}
FormatJS особенно эффективен в сочетании с code splitting.
const Dashboard = lazy(() => import('./Dashboard'))
Внутри dashboard:
import('./locales/dashboard/ru.json')
Пользователь не загружает:
до фактического открытия разделов.
Неправильно:
function getIntl() {
const cache = createIntlCache()
return createIntl(config, cache)
}
Правильно:
const cache = createIntlCache()
Неправильно:
global.intl = createIntl(...)
Это приводит к shared-state между запросами.
Неправильно:
import '@formatjs/intl-relativetimeformat/locale-data/ru'
import '@formatjs/intl-relativetimeformat/locale-data/en'
import '@formatjs/intl-relativetimeformat/locale-data/de'
Лучше:
await import(
`@formatjs/intl-relativetimeformat/locale-data/${locale}`
)
Типичная схема крупного приложения:
src/
i18n/
cache.js
loaders.js
intl.js
namespaces/
export const cache = createIntlCache()
export async function loadMessages(locale) {
return (
await import(`../locales/${locale}.json`)
).default
}
import {createIntl} from '@formatjs/intl'
import {cache} from './cache'
const intlMap = new Map()
export async function getIntl(locale) {
if (intlMap.has(locale)) {
return intlMap.get(locale)
}
const messages = await loadMessages(locale)
const intl = createIntl(
{
locale,
messages
},
cache
)
intlMap.set(locale, intl)
return intl
}
Ленивая инициализация особенно полезна в:
| Оптимизация | Эффект |
|---|---|
| Dynamic import | уменьшение bundle |
| Formatter cache | снижение CPU |
| Namespace loading | экономия памяти |
| Polyfill lazy loading | ускорение startup |
| Deferred initialization | снижение hydration cost |
Иногда удобно хранить registry всех локалей.
const localeRegistry = {
ru: () => import('./locales/ru.json'),
en: () => import('./locales/en.json'),
de: () => import('./locales/de.json')
}
Использование:
async function loadMessages(locale) {
return (await localeRegistry[locale]()).default
}
Каждый microfrontend может иметь собственный набор переводов.
export async function initPaymentsI18n(locale) {
const messages = await import(
`./payments/${locale}.json`
)
return createIntl({
locale,
messages: messages.default
})
}