При работе с локализацией данные переводов редко хранятся непосредственно внутри JavaScript-кода. Чаще используются отдельные JSON-файлы, загружаемые динамически по мере необходимости. Такой подход уменьшает размер начального бандла, позволяет лениво подгружать языки и разделять переводы по пространствам имён.
Библиотека i18next поддерживает асинхронную загрузку переводов через HTTP backend, а интеграция react-i18next тесно связана с механизмом Suspense из React.
При переключении языка библиотека может:
Пример структуры файлов:
public/
└── locales/
├── en/
│ ├── common.json
│ └── dashboard.json
└── ru/
├── common.json
└── dashboard.json
Каждый namespace загружается отдельно.
Для загрузки переводов по HTTP используется пакет:
npm install i18next-http-backend
Для React-проектов дополнительно обычно применяется:
npm install react-i18next
Файл инициализации:
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
i18n
.use(Backend)
.use(initReactI18next)
.init({
lng: 'ru',
fallbackLng: 'en',
ns: ['common'],
defaultNS: 'common',
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
},
interpolation: {
escapeValue: false
}
});
export default i18n;
Backend подключает механизм HTTP-загрузки.loadPath задаёт шаблон URL.{{lng}} заменяется текущим языком.{{ns}} заменяется namespace.Когда компонент использует переводы, которых ещё нет в памяти,
react-i18next может временно приостановить рендеринг.
Для этого используется Suspense.
Пример:
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import './i18n';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(
<Suspense fallback={<div>Загрузка переводов...</div>}>
<App />
</Suspense>
);
Последовательность работы:
Компонент вызывает useTranslation.
react-i18next проверяет наличие namespace.
Если namespace отсутствует:
После завершения загрузки интерфейс перерисовывается.
Пример компонента:
import { useTranslation } from 'react-i18next';
function Dashboard() {
const { t } = useTranslation('dashboard');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
export default Dashboard;
Если namespace dashboard ещё не загружен:
/locales/ru/dashboard.json
будет выполнен HTTP-запрос.
Одна из главных причин использования Suspense — возможность ленивой загрузки.
Например:
import { lazy, Suspense } from 'react';
const AdminPage = lazy(() => import('./AdminPage'));
function App() {
return (
<Suspense fallback={<div>Загрузка...</div>}>
<AdminPage />
</Suspense>
);
}
Если внутри AdminPage используется:
useTranslation('admin')
то одновременно могут загружаться:
Это уменьшает размер первоначального бандла.
Частая архитектура:
route chunk
+
translation namespace
Для каждого маршрута:
Пример:
/dashboard
dashboard.js
dashboard.json
/settings
settings.js
settings.json
Такой подход особенно полезен в:
Иногда Suspense использовать неудобно:
В этом случае Suspense можно отключить:
i18n.init({
react: {
useSuspense: false
}
});
При отключённом Suspense состояние загрузки нужно обрабатывать самостоятельно.
Пример:
import { useTranslation } from 'react-i18next';
function Profile() {
const { t, ready } = useTranslation('profile');
if (!ready) {
return <div>Загрузка...</div>;
}
return (
<div>
<h1>{t('title')}</h1>
</div>
);
}
Флаг ready показывает:
| Подход | Поведение |
|---|---|
| Suspense | React автоматически приостанавливает рендер |
| ready | Разработчик вручную управляет загрузкой |
Suspense уменьшает количество boilerplate-кода, но требует поддержки React Suspense.
Иногда необходимо заранее загрузить переводы.
Для этого используется:
i18n.loadNamespaces(['dashboard', 'profile']);
Пример:
await i18n.loadNamespaces('dashboard');
После этого компонент сможет отрендериться без fallback.
Можно заранее загрузить другой язык:
await i18n.loadLanguages(['en']);
Это полезно:
Пример:
import i18n from './i18n';
async function changeLanguage(lang) {
await i18n.changeLanguage(lang);
}
Если переводы отсутствуют:
Метод:
i18n.changeLanguage('de')
выполняет:
После загрузки namespace сохраняются в памяти.
Повторный запрос не выполняется:
useTranslation('dashboard');
если namespace уже был загружен ранее.
Пример:
const { t } = useTranslation([
'common',
'dashboard',
'charts'
]);
При первом рендере будут загружены все перечисленные namespace.
Плохой сценарий:
Компонент A
-> загрузка namespace A
После рендера:
Компонент B
-> загрузка namespace B
Возникает каскад последовательных запросов.
Лучше заранее объявлять необходимые namespace:
useTranslation([
'dashboard',
'charts',
'widgets'
]);
или предзагружать их:
await i18n.loadNamespaces([
'dashboard',
'charts',
'widgets'
]);
Backend может вернуть:
Можно отслеживать события:
i18n.on('failedLoading', (lng, ns, msg) => {
console.error(lng, ns, msg);
});
Если язык отсутствует:
/locales/de/common.json -> 404
будет использоваться:
fallbackLng: 'en'
Можно определить fallback namespace:
i18n.init({
fallbackNS: 'common'
});
Если ключ отсутствует:
t('save')
библиотека попробует найти его в common.
В больших приложениях полезно создавать несколько boundaries.
Пример:
<Suspense fallback={<PageLoader />}>
<Dashboard />
</Suspense>
или:
<Suspense fallback={<SidebarLoader />}>
<Sidebar />
</Suspense>
Это позволяет:
Частая архитектура:
<Suspense fallback={<AppLoader />}>
<App />
</Suspense>
Недостаток:
Более гибкий подход:
<App>
<Header />
<Suspense fallback={<WidgetLoader />}>
<AnalyticsWidget />
</Suspense>
</App>
Тогда загрузка переводов влияет только на конкретную часть UI.
При SSR Suspense требует особой настройки.
Основные проблемы:
На сервере обычно:
await i18n.loadNamespaces([
'common',
'dashboard'
]);
Только после этого выполняется render.
Популярный подход SSR:
Это позволяет избежать повторной загрузки.
Иногда используется несколько источников переводов:
Для этого применяется chained backend.
Пример архитектуры:
localStorage
↓
CDN
↓
API
Пакет:
npm install i18next-localstorage-backend
Позволяет:
Пример:
import i18n from 'i18next';
import ChainedBackend from 'i18next-chained-backend';
import HttpBackend from 'i18next-http-backend';
import LocalStorageBackend from 'i18next-localstorage-backend';
i18n
.use(ChainedBackend)
.init({
backend: {
backends: [
LocalStorageBackend,
HttpBackend
],
backendOptions: [
{
expirationTime: 7 * 24 * 60 * 60 * 1000
},
{
loadPath: '/locales/{{lng}}/{{ns}}.json'
}
]
}
});
Большие JSON-файлы ухудшают производительность.
Рекомендуется:
common.json;i18next умеет загружать namespace параллельно.
Пример:
useTranslation([
'common',
'dashboard',
'charts'
]);
Backend выполнит несколько запросов одновременно.
Для уменьшения числа запросов используется batching backend.
Пример:
/locales/resources.json?lng=ru&ns=common,dashboard,charts
Это снижает:
Основные события:
i18n.on('loaded', handler);
i18n.on('failedLoading', handler);
i18n.on('languageChanged', handler);
Пример:
i18n.on('loaded', (loaded) => {
console.log(loaded);
});
Содержит информацию о загруженных ресурсах.
Быстрое переключение:
ru -> en -> de -> fr
может приводить к конкурирующим запросам.
Современные версии i18next корректно обрабатывают такие сценарии, но backend должен поддерживать отмену или игнорирование устаревших ответов.
В React 18 Suspense тесно связан с concurrent rendering.
Это позволяет:
Пример:
import { startTransition } from 'react';
function switchLanguage(lang) {
startTransition(() => {
i18n.changeLanguage(lang);
});
}
React помечает обновление как некритичное.
Во время смены языка полезно отображать состояние:
const [loading, setLoading] = useState(false);
async function changeLang(lang) {
setLoading(true);
await i18n.changeLanguage(lang);
setLoading(false);
}
Неправильное использование может вызывать:
Полезные подходы:
Плохой вариант:
fallback={<div>Loading...</div>}
Лучше:
fallback={<DashboardSkeleton />}
Каждый микрофронтенд может:
Важно избегать:
Для анализа загрузки полезен debug:
i18n.init({
debug: true
});
В консоли будут отображаться:
Можно проверить наличие ресурсов:
i18n.hasResourceBundle('ru', 'dashboard');
Иногда ресурсы добавляются динамически:
i18n.addResourceBundle(
'ru',
'dashboard',
{
title: 'Панель'
}
);
После этого namespace считается загруженным.
Некоторые backend-системы поддерживают:
Это особенно актуально для очень больших приложений.
Типичная структура:
src/
├── i18n/
│ ├── index.js
│ ├── backends/
│ ├── detectors/
│ └── config/
│
├── locales/
│ ├── en/
│ ├── ru/
│ └── de/
│
└── pages/
├── dashboard/
├── profile/
└── settings/
Хорошо:
dashboard.json
profile.json
settings.json
Плохо:
translations.json
Лучше локализовать boundaries.
Например:
Особенно важно для:
Это предотвращает пустой UI при ошибках.
Очень большие translation-файлы ухудшают: