Кастомные resolvers

Механизм резолвинга в i18next отвечает за определение того, какой именно ресурс перевода должен быть использован в конкретный момент выполнения приложения. Это включает выбор языка, неймспейса, ключа, варианта множественного числа, fallback-языков и источника загрузки ресурсов.

Резолверы формируют слой логики между вызовом t('key') и фактическим получением строки из хранилища переводов. Внутри этой системы можно выделить несколько уровней:

  • резолвинг языка (language resolution)
  • резолвинг fallback-цепочек
  • резолвинг неймспейсов
  • резолвинг ключей и их составных частей
  • резолвинг ресурсов (backend resolution)

Кастомизация каждого из этих уровней позволяет адаптировать i18next под сложные сценарии: микрофронтенды, multi-tenant архитектуры, динамические CDN-источники, модульные пакеты переводов.


Механизм выбора языка и переопределение логики

В стандартной конфигурации i18next язык определяется через:

  • init параметр lng
  • i18next-browser-languagedetector
  • fallback цепочку fallbackLng

Внутри используется 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 resolution chain

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-коду, но и по бизнес-домену:

  • региональные варианты
  • брендовые языковые наборы
  • A/B тестирование переводов

Резолвинг неймспейсов и модульная структура переводов

i18next поддерживает разделение переводов на namespaces:

common.json
auth.json
dashboard.json

При вызове:

t('login.button', { ns: 'auth' })

происходит резолвинг:

  1. определение namespace
  2. выбор ресурса в памяти
  3. загрузка при отсутствии

Кастомизация резолвера 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
});

Кастомные key resolvers и стратегия поиска переводов

Ключ в i18next может быть:

  • простым: title
  • составным: auth.login.title
  • с контекстом: button_save

Внутри используется стратегия поиска:

  1. exact match
  2. namespace match
  3. fallback language match
  4. deep object traversal

Кастомный 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 resolver (загрузка переводов)

Наиболее мощный уровень кастомизации — backend resolution. Он отвечает за то, откуда и как загружаются переводы.

Стандартные варианты:

  • i18next-http-backend
  • filesystem backend
  • in-memory 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'
    }
  });

Такой резолвер позволяет:

  • загружать переводы с CDN
  • использовать GraphQL вместо REST
  • подключать multi-tenant storage
  • реализовывать lazy-loading переводов

Резолвинг через CDN и версионирование ресурсов

В распределённых системах важна версия переводов. Кастомный 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));
}

Это позволяет:

  • откатывать переводы
  • тестировать новые версии языков
  • изолировать A/B наборы переводов

Кастомный resolver интерполяции и постобработки

Хотя 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;
    }
  }
});

Расширенные резолверы позволяют:

  • локализовать формат чисел
  • внедрять бизнес-форматы дат
  • подключать ICU-like поведение

Resolver цепочка внутри i18next

Внутренняя логика резолвинга проходит несколько этапов:

  1. Определение языка
  2. Определение namespace
  3. Поиск ключа в памяти ресурсов
  4. Применение fallback languages
  5. Применение post-processors
  6. Интерполяция значений
  7. Возврат финальной строки

Эта цепочка может быть частично переопределена через плагины и кастомные 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);

Плагины позволяют:

  • внедрять feature flags в переводы
  • менять строки на лету
  • реализовывать динамическую персонализацию интерфейса

Резолвинг в SSR и изоляция контекстов

В серверном рендеринге ключевую роль играет изоляция резолверов между запросами.

Каждый запрос должен иметь собственный экземпляр i18next:

function createI18nInstance(lang) {
  const i18n = i18next.createInstance();

  i18n.init({
    lng: lang,
    fallbackLng: 'en'
  });

  return i18n;
}

Это исключает:

  • утечки языкового состояния
  • конфликт кеша ресурсов
  • неправильный fallback resolution

Динамический resolver на основе feature flags

В современных приложениях перевод может зависеть от фичи:

function featureBasedLanguageResolver(user, featureFlags) {
  if (featureFlags.newLocaleSystem) {
    return user.preferredLocaleV2;
  }
  return user.locale;
}

Такая схема позволяет плавно мигрировать системы локализации без изменения бизнес-логики приложения.


Кастомизация резолвинга ресурсов в сложных архитектурах

В enterprise-системах часто используется распределённое хранение переводов:

  • микросервисы
  • отдельные языковые сервисы
  • CDN-слой
  • локальный кеш браузера

Кастомный resolver может агрегировать данные:

async function resolveResources(language, namespace) {
  const [cdn, local] = await Promise.all([
    fetchCDN(language, namespace),
    readLocalCache(language, namespace)
  ]);

  return {
    ...cdn,
    ...local
  };
}

Приоритет источников может быть:

  1. локальные overrides
  2. CDN глобальные переводы
  3. fallback static bundle

Поведение резолвера при отсутствии ключей

Если ключ не найден, i18next проходит fallback-цепочку. Кастомный резолвер может изменить поведение:

i18next.init({
  saveMissing: true,
  missingKeyHandler: (lng, ns, key) => {
    console.warn(`Missing key: ${key}`);
    return key;
  }
});

Это позволяет:

  • логировать отсутствующие переводы
  • автоматически собирать словари
  • генерировать заглушки

Композиция нескольких резолверов

В сложных системах резолверы комбинируются:

  • language resolver
  • namespace resolver
  • backend resolver
  • key resolver
  • interpolation resolver

Итоговая система представляет собой pipeline, где каждый слой может:

  • изменить входные данные
  • перенаправить запрос
  • вернуть финальный результат
  • передать управление дальше