Динамическое переключение языка

В экосистеме интернационализации JavaScript ключевое место занимает библиотека FormatJS, предоставляющая набор инструментов для форматирования дат, чисел, сообщений и управления локалями в приложениях. Одним из наиболее сложных аспектов прикладной i18n является динамическое переключение языка без перезагрузки приложения и без потери состояния интерфейса.

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


Базовая модель локализации в FormatJS

В основе подхода FormatJS лежит концепция контекстного провайдера локали и словаря сообщений. В React-приложениях эта модель обычно реализуется через IntlProvider, который распространяет текущую локаль и набор переводов по дереву компонентов.

Ключевые элементы:

  • locale — текущий язык интерфейса (en, ru, kk, de и т.д.)
  • messages — словарь переводов для выбранной локали
  • defaultLocale — язык по умолчанию
  • key (в React-обёртках) — механизм принудительного пересоздания контекста

Механизм смены языка на уровне состояния приложения

Динамическое переключение языка строится вокруг централизованного состояния локали. Обычно оно хранится в глобальном сторе или React state.

При изменении локали происходит:

  1. Обновление значения locale
  2. Подгрузка нового набора переводов messages
  3. Перерисовка дерева компонентов, зависящих от IntlProvider
  4. Переинициализация форматирования через Intl API

Простейшая модель состояния:

const [locale, setLocale] = useState('en');
const [messages, setMessages] = useState(enMessages);

Переключение локали через IntlProvider

Ключевой механизм обновления интерфейса — реактивная замена контекста:

import { IntlProvider } from "react-intl";

function App() {
  return (
    <IntlProvider locale={locale} messages={messages}>
      <MainApp />
    </IntlProvider>
  );
}

Однако этого недостаточно для корректного полного обновления всех компонентов. Важно учитывать, что некоторые форматеры кэшируются React-обёртками и могут не пересоздаваться при изменении контекста без дополнительного триггера.


Принудительное обновление дерева через ключ (key-based remount)

Одним из устойчивых паттернов является использование key на уровне провайдера:

<IntlProvider
  key={locale}
  locale={locale}
  messages={messages}
>
  <MainApp />
</IntlProvider>

Такой подход гарантирует:

  • полное размонтирование старого дерева
  • создание нового контекста Intl
  • пересоздание всех formatters
  • отсутствие «залипания» старых переводов

Этот метод особенно важен при сложных сценариях, где используется кэширование сообщений или сторонние компоненты с внутренним состоянием локализации.


Асинхронная загрузка переводов

В реальных приложениях переводы редко находятся в одном бандле. Чаще используется ленивое подгружение по требованию.

async function loadMessages(locale) {
  switch (locale) {
    case "ru":
      return import("./messages/ru.json");
    case "en":
      return import("./messages/en.json");
    case "de":
      return import("./messages/de.json");
  }
}

Процесс переключения языка:

async function changeLocale(newLocale) {
  const messages = await loadMessages(newLocale);

  setLocale(newLocale);
  setMessages(messages.default);
}

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

  • отмена предыдущих загрузок (AbortController)
  • игнорирование устаревших промисов
  • кэширование загруженных локалей

Синхронизация форматов с Intl API

FormatJS опирается на нативный Intl API браузера. При смене языка пересоздаются форматеры:

  • Intl.DateTimeFormat
  • Intl.NumberFormat
  • Intl.PluralRules
  • Intl.RelativeTimeFormat

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

Пример:

intl.formatNumber(1000);
// en → "1,000"
// de → "1.000"
// ru → "1 000"

Переключение языка без потери состояния интерфейса

Одна из сложных задач — сохранить состояние UI при смене языка. Перерисовка дерева может привести к:

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

Для минимизации эффектов применяются:

  • key только на IntlProvider, а не на корневом App
  • разделение локализации и бизнес-состояния
  • мемоизация тяжёлых компонентов

Обновление сообщений без ремоунта

Альтернативный подход — обновление messages без пересоздания дерева:

<IntlProvider locale={locale} messages={messages}>
  <MainApp />
</IntlProvider>

В этом случае изменение происходит через контекст React. Но некоторые сообщения могут не обновляться, если используются:

  • заранее вычисленные форматированные строки
  • мемоизированные hooks
  • внешние кеши переводов

Использование hooks и реактивность перевода

В FormatJS широко применяется useIntl:

import { useIntl } from "react-intl";

function Header() {
  const intl = useIntl();

  return <h1>{intl.formatMessage({ id: "title" })}</h1>;
}

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

  • изменился locale
  • изменился messages в IntlProvider

Важно учитывать, что строки, сохранённые в переменные, не обновляются автоматически:

const title = intl.formatMessage({ id: "title" }); // фиксируется при рендере

Переключение языка и pluralization

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

intl.formatMessage(
  { id: "items" },
  { count: 5 }
);

Для разных локалей:

  • английский: 5 items
  • русский: 5 элементов
  • арабский: сложные правила множественных форм

При смене языка пересоздаётся PluralRules, что влияет на выбор ветки сообщения.


Работа с fallback-локалью

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

  • отсутствующие ключи
  • частично загруженные переводы
  • деградация до defaultLocale

Поведение:

<IntlProvider
  locale="fr"
  defaultLocale="en"
  messages={messages}
>

Если перевод отсутствует, система использует fallback-значение или ID сообщения.


Согласованность локали в приложении

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

  • UI-компоненты
  • даты и время
  • API-запросы (Accept-Language)
  • форматирование чисел

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


Оптимизация переключения языка

При высокочастотной смене локали возникают проблемы производительности:

  • повторная инициализация Intl форматеров
  • повторный рендер больших деревьев
  • загрузка переводов

Применяются стратегии:

  • кеширование messages по локали
  • lazy hydration переводов
  • разделение критичных и вторичных переводов
  • предварительная загрузка ближайших локалей

Управление направлением текста (LTR/RTL)

Динамическая смена языка часто включает изменение направления интерфейса:

document.documentElement.dir = locale === "ar" ? "rtl" : "ltr";

Это влияет на:

  • layout
  • flex-направления
  • позиционирование элементов
  • иконографику

FormatJS сам не управляет RTL, но интегрируется с этим уровнем через внешнюю логику приложения.


Согласование маршрутизации и локали

В SPA-приложениях смена языка может быть связана с маршрутизацией:

  • /en/dashboard
  • /ru/dashboard

При изменении локали маршруты пересобираются, а данные страницы перезапрашиваются с учётом новой локали. Это требует синхронизации:

  • router state
  • IntlProvider state
  • кеша запросов

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

В больших приложениях переводы могут поступать фрагментами:

  • базовые UI строки
  • доменные тексты
  • ошибки API

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

  1. системные сообщения
  2. интерфейс
  3. доменные тексты
  4. пользовательские переводы

Это позволяет избежать «пустого интерфейса» при неполной загрузке локали.