Механизм резолвинга в i18next отвечает за определение того, какой именно ресурс перевода должен быть использован в конкретный момент выполнения приложения. Это включает выбор языка, неймспейса, ключа, варианта множественного числа, fallback-языков и источника загрузки ресурсов.
Резолверы формируют слой логики между вызовом t('key') и
фактическим получением строки из хранилища переводов. Внутри этой
системы можно выделить несколько уровней:
Кастомизация каждого из этих уровней позволяет адаптировать i18next под сложные сценарии: микрофронтенды, multi-tenant архитектуры, динамические CDN-источники, модульные пакеты переводов.
В стандартной конфигурации i18next язык определяется через:
init параметр lngi18next-browser-languagedetectorfallbackLngВнутри используется languageUtils, который строит
цепочку языков, например:
ru-KZ → ru → en
Кастомный резолвер языков может быть реализован через переопределение
детектора или через обёртку над languageUtils.
Пример логики кастомного определения языка:
import i18next from 'i18next';
function customLanguageResolver(headers, cookies, query) {
if (query.lang) return query.lang;
if (cookies.locale) return cookies.locale;
if (headers['x-lang']) return headers['x-lang'];
return 'en';
}
i18next.init({
lng: customLanguageResolver(
{ 'x-lang': 'ru' },
{ locale: 'kk' },
{}
),
fallbackLng: ['en']
});
В реальных системах подобная логика часто инкапсулируется в middleware (например, Express, NestJS), где результат резолвинга передаётся в i18next как контекст запроса.
Fallback-цепочка определяет, какие языки будут использоваться при отсутствии ключа.
Стандартное поведение:
ru-KZ → ru → en
Кастомный резолвер fallback может быть задан через функцию:
i18next.init({
fallbackLng: (code) => {
if (code.startsWith('ru-')) return ['ru', 'en'];
if (code.startsWith('kk-')) return ['kk', 'ru', 'en'];
return ['en'];
}
});
Такой подход полезен в системах, где языки логически сгруппированы не только по ISO-коду, но и по бизнес-домену:
i18next поддерживает разделение переводов на namespaces:
common.json
auth.json
dashboard.json
При вызове:
t('login.button', { ns: 'auth' })
происходит резолвинг:
Кастомизация резолвера namespaces особенно важна в микрофронтендах.
Пример динамического namespace resolver:
function resolveNamespace(path) {
if (path.startsWith('auth.')) return 'auth';
if (path.startsWith('admin.')) return 'admin';
return 'common';
}
i18next.init({
defaultNS: 'common',
ns: ['common', 'auth', 'admin'],
nsSeparator: false
});
Ключ в i18next может быть:
titleauth.login.titlebutton_saveВнутри используется стратегия поиска:
Кастомный key resolver может переопределять логику доступа к структуре ресурсов:
function customKeyResolver(obj, key) {
const parts = key.split('|');
let current = obj;
for (const part of parts) {
if (!current) return undefined;
current = current[part];
}
return current;
}
Использование нестандартного разделителя позволяет интегрировать i18next с существующими форматами JSON, где ключи уже заданы по бизнес-логике.
Наиболее мощный уровень кастомизации — backend resolution. Он отвечает за то, откуда и как загружаются переводы.
Стандартные варианты:
i18next-http-backendКастомный backend реализуется через интерфейс:
class CustomBackend {
constructor(services, options = {}) {
this.services = services;
this.options = options;
}
read(language, namespace, callback) {
fetch(`${this.options.host}/locales/${language}/${namespace}`)
.then(res => res.json())
.then(data => callback(null, data))
.catch(err => callback(err));
}
create() {}
type = 'backend';
}
И регистрация:
import i18next from 'i18next';
i18next
.use(CustomBackend)
.init({
backend: {
host: 'https://cdn.example.com'
}
});
Такой резолвер позволяет:
В распределённых системах важна версия переводов. Кастомный resolver может добавлять versioning слой:
function buildPath(language, namespace, version) {
return `https://cdn.site.com/${version}/${language}/${namespace}.json`;
}
Интеграция в backend:
read(language, namespace, callback) {
const version = this.options.versionResolver(language);
const url = buildPath(language, namespace, version);
fetch(url)
.then(r => r.json())
.then(data => callback(null, data))
.catch(err => callback(err));
}
Это позволяет:
Хотя interpolation формально не является backend-resolver, она участвует в цепочке разрешения итогового значения строки.
Стандарт:
t('welcome', { name: 'Alex' })
Кастомизация:
i18next.init({
interpolation: {
format: (value, format) => {
if (format === 'uppercase') return value.toUpperCase();
if (format === 'currency') return `$${value}`;
return value;
}
}
});
Расширенные резолверы позволяют:
Внутренняя логика резолвинга проходит несколько этапов:
Эта цепочка может быть частично переопределена через плагины и кастомные middleware.
i18next plugin system позволяет внедрять собственные резолверы на любом уровне.
Пример плагина, изменяющего поведение получения ресурсов:
const CustomResolverPlugin = {
type: 'postProcessor',
name: 'customResolver',
process(value, key, options) {
if (options.context === 'admin') {
return `[ADMIN] ${value}`;
}
return value;
}
};
i18next.use(CustomResolverPlugin);
Плагины позволяют:
В серверном рендеринге ключевую роль играет изоляция резолверов между запросами.
Каждый запрос должен иметь собственный экземпляр i18next:
function createI18nInstance(lang) {
const i18n = i18next.createInstance();
i18n.init({
lng: lang,
fallbackLng: 'en'
});
return i18n;
}
Это исключает:
В современных приложениях перевод может зависеть от фичи:
function featureBasedLanguageResolver(user, featureFlags) {
if (featureFlags.newLocaleSystem) {
return user.preferredLocaleV2;
}
return user.locale;
}
Такая схема позволяет плавно мигрировать системы локализации без изменения бизнес-логики приложения.
В enterprise-системах часто используется распределённое хранение переводов:
Кастомный resolver может агрегировать данные:
async function resolveResources(language, namespace) {
const [cdn, local] = await Promise.all([
fetchCDN(language, namespace),
readLocalCache(language, namespace)
]);
return {
...cdn,
...local
};
}
Приоритет источников может быть:
Если ключ не найден, i18next проходит fallback-цепочку. Кастомный резолвер может изменить поведение:
i18next.init({
saveMissing: true,
missingKeyHandler: (lng, ns, key) => {
console.warn(`Missing key: ${key}`);
return key;
}
});
Это позволяет:
В сложных системах резолверы комбинируются:
Итоговая система представляет собой pipeline, где каждый слой может: