Мемоизация переводов

Мемоизация переводов в i18next — это совокупность механизмов и практик, направленных на уменьшение повторных вычислений результата функции t при одинаковых входных данных. В отличие от классической мемоизации чистых функций, здесь учитываются дополнительные факторы: язык, namespace, параметры интерполяции, контекст, формы множественного числа и постобработка.

Функция перевода формально выглядит как:

t(key, options)

Результат зависит не только от key, но и от текущего языка, конфигурации ресурсов и параметров options. Поэтому мемоизация в i18next — это многомерное кеширование.


Внутренняя модель кеширования ресурсов

i18next использует несколько уровней хранения данных, которые уже обеспечивают частичную мемоизацию:

  1. Загрузка ресурсов Переводы загружаются по языкам и namespace и сохраняются в памяти:
resources = {
  ru: {
    translation: {
      hello: "Привет"
    }
  }
}
  1. Доступ к ресурсу При вызове t('hello') происходит прямой доступ к объекту словаря без повторной загрузки или пересборки структуры.

  2. Кеширование разрешения ключей Внутри i18next используется оптимизация поиска по ключам, чтобы не обходить дерево переводов при каждом вызове.


Ключ кеша перевода

Результат t() можно рассматривать как функцию от набора параметров:

  • ключ перевода
  • язык (lng)
  • namespace
  • контекст (например, gender, formality)
  • count (для pluralization)
  • interpolation values
  • postprocessors

Условная модель кеш-ключа:

cacheKey = key + lng + ns + JSON.stringify(optionsSubset)

При этом важно, что разные вызовы:

t('item', { count: 1 })
t('item', { count: 2 })

дают разные результаты и не могут быть объединены в один кеш-элемент.


Интерполяция и влияние на производительность

Интерполяция — одна из самых затратных частей в процессе перевода, особенно при больших объектах:

t('welcome_user', { name: 'Alex', age: 30 })

Шаблон:

{
  "welcome_user": "Hello {{name}}, age {{age}}"
}

Каждый вызов требует:

  • поиска строки шаблона
  • парсинга плейсхолдеров (если не закешировано)
  • подстановки значений

i18next частично оптимизирует этот процесс через внутренние кеши интерполяции шаблонов, чтобы не пересобирать парсер для одного и того же ключа.


Инвалидация кеша при смене языка

Смена языка является глобальным триггером инвалидирования результатов:

i18next.changeLanguage('en')

После этого:

  • все ранее рассчитанные переводы становятся неактуальными
  • t() начинает использовать другой набор ресурсов
  • любые внешние мемоизации по ключу без учета языка становятся источником ошибок

Типичная ошибка:

const cache = new Map();

function translate(key) {
  if (cache.has(key)) return cache.get(key);
  const value = i18next.t(key);
  cache.set(key, value);
  return value;
}

Такой кеш некорректен, поскольку игнорирует lng.

Корректный вариант:

function makeCacheKey(key, lng) {
  return `${lng}:${key}`;
}

React и мемоизация переводов

В связке с React наиболее важный аспект — контроль перерисовок.

Хук:

const { t, i18n } = useTranslation();

Возвращает t, который зависит от текущего языка через контекст i18next.

Проблема лишних ререндеров

function Title() {
  const { t } = useTranslation();
  return <h1>{t('title')}</h1>;
}

При смене языка компонент перерисуется, даже если ключи не изменились.


useMemo и стабилизация вычислений

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

const label = useMemo(() => {
  return t('complex_label', { value });
}, [t, value]);

Однако важно учитывать: t меняется при смене языка, поэтому это корректно инвалидирует кеш.


Мемоизация списков переводов

Частая задача — рендер списков:

const items = ['home', 'about', 'contact'];

Без мемоизации:

items.map(key => <li key={key}>{t(key)}</li>);

Оптимизированный вариант:

const translatedItems = useMemo(() => {
  return items.map(key => t(key));
}, [i18n.language]);

Здесь зависимость от языка важнее, чем от t.


Кеширование через namespaces

Разделение переводов на namespaces снижает объем повторных операций:

i18next.init({
  ns: ['common', 'auth', 'profile'],
  defaultNS: 'common'
});

При загрузке:

  • каждый namespace кешируется отдельно
  • доступ к t('key', { ns: 'auth' }) ограничивает область поиска
  • уменьшается стоимость lookup в больших словарях

Внешние стратегии кеширования

Помимо внутреннего механизма i18next, применяются внешние слои кеширования.

Кеш ресурсов в браузере

import Backend from 'i18next-http-backend';

i18next.use(Backend).init({
  backend: {
    loadPath: '/locales/{{lng}}/{{ns}}.json',
    requestOptions: {
      cache: 'force-cache'
    }
  }
});

LocalStorage кеш

Часто используется плагин:

  • сохранение загруженных переводов
  • уменьшение сетевых запросов
  • ускорение первичной инициализации

Мемоизация форматирования и постобработки

i18next поддерживает постпроцессоры:

t('price', { postProcess: 'currency' })

Такие операции не кешируются автоматически, так как:

  • зависят от внешних правил форматирования
  • могут использовать runtime-данные (Intl API)

Поэтому мемоизация таких результатов требует отдельного слоя:

const formatCache = new Map();

function formatPrice(value, lng) {
  const key = `${lng}:${value}`;
  if (formatCache.has(key)) return formatCache.get(key);

  const result = new Intl.NumberFormat(lng, {
    style: 'currency',
    currency: 'USD'
  }).format(value);

  formatCache.set(key, result);
  return result;
}

Динамические значения и ограничения кеширования

Наибольшую сложность представляют динамические переводы:

t('items_in_cart', { count, itemName })

Проблемы:

  • большое количество уникальных комбинаций options
  • невозможность агрессивного кеширования
  • риск переполнения Map/WeakMap кешей

Поэтому используется стратегия:

  • кешировать только шаблон
  • не кешировать финальную строку при высокой вариативности

Практика безопасной мемоизации

Корректные подходы учитывают язык и namespace:

const translationCache = new Map();

function translate(key, lng, options = {}) {
  const cacheKey = `${lng}:${key}:${JSON.stringify(options)}`;

  if (translationCache.has(cacheKey)) {
    return translationCache.get(cacheKey);
  }

  const result = i18next.t(key, options);
  translationCache.set(cacheKey, result);

  return result;
}

Более устойчивый вариант — ограничение кеширования только статических ключей:

const staticCache = new Map();

function tStatic(key) {
  const lng = i18next.language;
  const cacheKey = `${lng}:${key}`;

  if (staticCache.has(cacheKey)) return staticCache.get(cacheKey);

  const result = i18next.t(key);
  staticCache.set(cacheKey, result);

  return result;
}

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

При lazy-loading namespaces:

  • первый вызов t() может быть асинхронно “дорогим”
  • последующие вызовы используют уже загруженный ресурс
  • кеш становится эффективным только после завершения загрузки

Типичный сценарий:

await i18next.loadNamespaces('profile');
t('profile:title');

До загрузки кеширование ограничено отсутствием данных, после — работает в полном объеме.


Влияние ICU и plural rules

При использовании множественных форм:

t('item', { count: 3 })

i18next применяет:

  • CLDR plural rules
  • выбор формы строки
  • кеширование результата зависит от count

Фактически формируется отдельный кеш по каждому значению count, что увеличивает гранулярность кеша.