Сохранение выбора пользователя

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

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


Принцип устойчивости языкового выбора

Поведение системы интернационализации строится вокруг трёх состояний:

  • язык, определённый автоматически при первом визите;
  • язык, выбранный пользователем вручную;
  • язык, восстановленный из предыдущей сессии.

Ключевая задача — обеспечить приоритет пользовательского выбора над всеми остальными источниками.

Типичная иерархия:

  1. сохранённый язык (cookie / localStorage);
  2. язык из URL (querystring);
  3. язык браузера (navigator.language);
  4. fallbackLng (язык по умолчанию приложения).

Механизм Language Detector

В i18next используется модуль i18next-browser-languagedetector, который отвечает за извлечение языка из различных источников.

Основные источники:

  • cookie
  • localStorage
  • sessionStorage
  • querystring
  • navigator
  • htmlTag

Конфигурация определяет порядок проверки:

import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

i18n
  .use(LanguageDetector)
  .init({
    detection: {
      order: ['localStorage', 'cookie', 'querystring', 'navigator', 'htmlTag'],
      caches: ['localStorage', 'cookie']
    },
    fallbackLng: 'en',
    resources: {
      en: { translation: {} },
      ru: { translation: {} }
    }
  });

Сохранение языка в localStorage

localStorage является наиболее простым и распространённым способом сохранения выбора пользователя.

При изменении языка i18next автоматически записывает значение:

i18n.changeLanguage('ru');

После вызова происходит:

  • обновление внутреннего состояния i18next;
  • сохранение значения в localStorage (если включён cache);
  • повторный рендер интерфейса.

Можно явно задать ключ хранения:

detection: {
  order: ['localStorage'],
  lookupLocalStorage: 'app_language',
  caches: ['localStorage']
}

Cookies используются в сценариях, где требуется серверная синхронизация языка (SSR, multi-domain приложения).

Конфигурация:

detection: {
  order: ['cookie'],
  lookupCookie: 'i18next',
  caches: ['cookie'],
  cookieMinutes: 60 * 24 * 365
}

Параметры:

  • lookupCookie — имя cookie;
  • cookieMinutes — срок хранения;
  • cookieDomain — домен для доступности cookie.

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


Использование querystring

Язык может передаваться через URL:

https://example.com?lng=ru

Конфигурация:

detection: {
  order: ['querystring'],
  lookupQuerystring: 'lng'
}

Этот механизм применяется для:

  • маркетинговых ссылок;
  • шаринга страниц;
  • A/B тестирования локалей.

Ручное управление сохранением языка

Несмотря на автоматическое сохранение через detector, часто требуется явное управление состоянием.

Изменение языка:

i18n.changeLanguage('de');

После этого происходит:

  • обновление текущего языка;
  • триггер событий languageChanged;
  • запись в выбранное хранилище (если подключены caches).

Прослушивание изменений:

i18n.on('languageChanged', (lng) => {
  console.log('Текущий язык:', lng);
});

Это позволяет синхронизировать язык с внешними системами состояния.


Полностью кастомный механизм хранения

В случаях, когда требуется нестандартное хранилище (например, IndexedDB или серверный профиль пользователя), используется кастомный language detector.

Пример реализации:

const customDetector = {
  name: 'customDetector',

  lookup() {
    return sessionStorage.getItem('lang') || 'en';
  },

  cacheUserLanguage(lng) {
    sessionStorage.setItem('lang', lng);
  }
};

i18n
  .use({
    type: 'languageDetector',
    init() {},
    detect: () => customDetector.lookup(),
    cacheUserLanguage: (lng) => customDetector.cacheUserLanguage(lng)
  })
  .init();

Такая модель позволяет полностью отделить i18next от браузерных ограничений.


Приоритет источников и конфликтные ситуации

При одновременном наличии нескольких источников возникает конфликт определения языка.

Пример конфигурации:

detection: {
  order: ['querystring', 'cookie', 'localStorage', 'navigator'],
  caches: ['localStorage', 'cookie']
}

Логика:

  • querystring имеет максимальный приоритет (временное переопределение);
  • cookie сохраняет пользовательский выбор между сессиями;
  • localStorage используется как быстрый клиентский кеш;
  • navigator применяется только как fallback.

Работа с fallbackLng

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

fallbackLng: 'en'

Также возможна более сложная структура:

fallbackLng: {
  'ru-KZ': ['ru', 'en'],
  default: ['en']
}

Это позволяет учитывать региональные вариации языков.


Синхронизация между вкладками

При использовании localStorage изменение языка в одной вкладке не всегда отражается в других. Для синхронизации применяется событие storage:

window.addEventListener('storage', (event) => {
  if (event.key === 'i18nextLng') {
    i18n.changeLanguage(event.newValue);
  }
});

Это обеспечивает единое состояние интерфейса во всех открытых вкладках.


SSR и восстановление языка на сервере

В серверных приложениях язык должен определяться до рендера страницы.

Типичный подход:

  • извлечение cookie из запроса;
  • передача языка в i18n.init;
  • рендер HTML уже с нужной локалью.
i18n.init({
  lng: req.cookies.i18next || 'en',
  resources,
  fallbackLng: 'en'
});

Это устраняет «мигание» языка при гидратации клиента.


Порядок и устойчивость состояния

Стабильное поведение достигается за счёт строгого соблюдения порядка:

  • источник URL (если есть);
  • сохранённое значение;
  • браузерные настройки;
  • значение по умолчанию.

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


Особенности работы с отключённым хранилищем

В условиях строгой приватности браузера (инкогнито, блокировка cookies, ограниченный localStorage) сохранение языка может быть недоступно.

В таких случаях:

  • используется только runtime-состояние i18next;
  • язык сбрасывается при перезагрузке;
  • fallbackLng становится единственным устойчивым источником.

Интеграция с UI-состоянием приложения

Изменение языка часто связано с глобальным состоянием интерфейса:

const handleLanguageSwitch = (lng) => {
  i18n.changeLanguage(lng);
  setAppState(prev => ({ ...prev, language: lng }));
};

Такой подход обеспечивает синхронизацию перевода, форматирования дат и чисел, а также других локализованных компонентов.


Обработка неподдерживаемых языков

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

  • проверка наличия в resources;
  • переход к ближайшему языку;
  • использование fallbackLng.

Пример:

  • сохранён fr-CA;
  • доступен только fr;
  • будет использован fr.