Переключение языков в React

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

В экосистеме React чаще всего используется связка:

  • i18next
  • react-i18next

Типичная структура:

src/
 ├── i18n.js
 ├── locales/
 │    ├── en/
 │    │    └── translation.json
 │    └── ru/
 │         └── translation.json
 └── components/
      └── LanguageSwitcher.jsx

Инициализация i18next

Файл конфигурации:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import ru from './locales/ru/translation.json';
import en from './locales/en/translation.json';

i18n
  .use(initReactI18next)
  .init({
    resources: {
      ru: {
        translation: ru
      },
      en: {
        translation: en
      }
    },

    lng: 'ru',
    fallbackLng: 'en',

    interpolation: {
      escapeValue: false
    }
  });

export default i18n;

Основные параметры:

Параметр Назначение
resources Хранилище переводов
lng Текущий язык
fallbackLng Резервный язык
interpolation Настройки подстановки значений

Подключение конфигурации

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

import React from 'react';
import ReactDOM from 'react-dom/client';

import App from './App';
import './i18n';

const root = ReactDOM.createRoot(
  document.getElementById('root')
);

root.render(<App />);

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


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

Пример компонента:

import { useTranslation } from 'react-i18next';

function Header() {
  const { t } = useTranslation();

  return (
    <h1>{t('welcome')}</h1>
  );
}

export default Header;

JSON-файл перевода:

{
  "welcome": "Добро пожаловать"
}

Английская версия:

{
  "welcome": "Welcome"
}

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

Главный механизм смены локали — метод changeLanguage.

i18n.changeLanguage('en');

После вызова:

  1. изменяется текущий язык;
  2. обновляется внутреннее состояние i18next;
  3. React-компоненты автоматически перерисовываются;
  4. отображаются новые переводы.

Создание компонента переключателя языка

Простейший переключатель:

import { useTranslation } from 'react-i18next';

function LanguageSwitcher() {
  const { i18n } = useTranslation();

  const switchToRussian = () => {
    i18n.changeLanguage('ru');
  };

  const switchToEnglish = () => {
    i18n.changeLanguage('en');
  };

  return (
    <div>
      <button onCl ick={switchToRussian}>
        RU
      </button>

      <button onCl ick={switchToEnglish}>
        EN
      </button>
    </div>
  );
}

export default LanguageSwitcher;

Текущий язык приложения

Текущая локаль хранится в:

i18n.language

Пример:

const currentLanguage = i18n.language;

console.log(currentLanguage);

Условный рендеринг:

{
  i18n.language === 'ru'
    ? 'Русский'
    : 'English'
}

Универсальный переключатель

Более масштабируемый вариант:

import { useTranslation } from 'react-i18next';

function LanguageSwitcher() {
  const { i18n } = useTranslation();

  const changeLanguage = (lng) => {
    i18n.changeLanguage(lng);
  };

  return (
    <div>
      <button onCl ick={() => changeLanguage('ru')}>
        RU
      </button>

      <button onCl ick={() => changeLanguage('en')}>
        EN
      </button>

      <button onCl ick={() => changeLanguage('de')}>
        DE
      </button>
    </div>
  );
}

export default LanguageSwitcher;

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


Переключение через select

Наиболее распространённый интерфейс:

import { useTranslation } from 'react-i18next';

function LanguageSelect() {
  const { i18n } = useTranslation();

  const handleChange = (event) => {
    i18n.changeLanguage(event.target.value);
  };

  return (
    <sel ect
      value={i18n.language}
      onCha nge={handleChange}
    >
      <option value="ru">Русский</option>
      <option value="en">English</option>
      <option value="de">Deutsch</option>
    </select>
  );
}

export default LanguageSelect;

Преимущества:

  • компактность;
  • удобство масштабирования;
  • простая интеграция с UI-библиотеками.

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

Без сохранения после обновления страницы язык сбрасывается. Обычно используются:

  • localStorage;
  • cookies;
  • определение языка браузера.

Сохранение через localStorage

При смене языка:

const changeLanguage = (lng) => {
  localStorage.setItem('language', lng);

  i18n.changeLanguage(lng);
};

При инициализации:

const savedLanguage =
  localStorage.getItem('language') || 'ru';

Полная конфигурация:

i18n.init({
  resources,

  lng: localStorage.getItem('language') || 'ru',

  fallbackLng: 'en',

  interpolation: {
    escapeValue: false
  }
});

Автоматическое определение языка браузера

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

npm install i18next-browser-languagedetector

Подключение:

import LanguageDetector
  fr om 'i18next-browser-languagedetector';

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en'
  });

Теперь библиотека автоматически определяет:

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

Порядок определения языка

Настраивается параметром order.

detection: {
  order: [
    'localStorage',
    'navigator',
    'htmlTag'
  ]
}

Приоритет:

  1. localStorage;
  2. настройки браузера;
  3. атрибут <html lang="">.

Кэширование языка

Настройка сохранения:

detection: {
  caches: ['localStorage']
}

Теперь выбранная локаль автоматически сохраняется.


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

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

Установка backend-плагина:

npm install i18next-http-backend

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

import Backend from 'i18next-http-backend';

i18n
  .use(Backend)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',

    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json'
    }
  });

Структура:

public/
 └── locales/
      ├── en/
      │    └── translation.json
      └── ru/
           └── translation.json

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

При вызове:

i18n.changeLanguage('de');

библиотека:

  1. запрашивает файл перевода;
  2. загружает JSON;
  3. обновляет словарь;
  4. инициирует повторный рендер.

Индикация загрузки переводов

При ленивой загрузке может возникнуть задержка. React Suspense позволяет отображать fallback.

import React, { Suspense } from 'react';

root.render(
  <Suspense fallback={<div>Loading...</div>}>
    <App />
  </Suspense>
);

Namespaces

В больших проектах переводы разделяются по модулям.

Пример:

locales/
 ├── ru/
 │    ├── common.json
 │    ├── auth.json
 │    └── dashboard.json
 └── en/

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

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

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

const { t } = useTranslation('auth');

t('login');

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

При смене локали i18next автоматически:

  • загружает namespace нужного языка;
  • обновляет связанные компоненты;
  • кэширует переводы.

Изменение атрибута lang

Важно синхронизировать язык интерфейса с DOM.

i18n.on('languageChanged', (lng) => {
  document.documentElement.lang = lng;
});

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

  • SEO;
  • screen readers;
  • голосовой ввод;
  • браузерные инструменты перевода.

Поддержка RTL-языков

Для арабского и иврита требуется изменение направления текста.

i18n.on('languageChanged', (lng) => {
  const dir = lng === 'ar'
    ? 'rtl'
    : 'ltr';

  document.documentElement.dir = dir;
});

Конфигурация нескольких RTL-языков

Более универсальный вариант:

const rtlLanguages = ['ar', 'he', 'fa'];

i18n.on('languageChanged', (lng) => {
  document.documentElement.dir =
    rtlLanguages.includes(lng)
      ? 'rtl'
      : 'ltr';
});

Использование контекста React

Хотя react-i18next уже использует Context API, иногда требуется собственная обёртка.

Пример:

import { createContext } from 'react';

export const LanguageContext =
  createContext();

Provider:

<LanguageContext.Provider
  value={{
    language: i18n.language,
    changeLanguage: i18n.changeLanguage
  }}
>
  <App />
</LanguageContext.Provider>

Такой подход полезен при интеграции:

  • Redux;
  • Zustand;
  • MobX;
  • серверной логики.

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

Пример action:

export const setLanguage = (language) => ({
  type: 'SET_LANGUAGE',
  payload: language
});

Reducer:

const initialState = {
  language: 'ru'
};

export default function reducer(
  state = initialState,
  action
) {
  switch (action.type) {
    case 'SET_LANGUAGE':
      return {
        ...state,
        language: action.payload
      };

    default:
      return state;
  }
}

Синхронизация:

store.subscribe(() => {
  const state = store.getState();

  i18n.changeLanguage(state.language);
});

Переключение языка по URL

Часто используется структура:

/en/dashboard
/ru/dashboard
/de/dashboard

Пример с React Router:

<Route path="/:lng/dashboard" />

Получение языка:

const { lng } = useParams();

useEffect(() => {
  i18n.changeLanguage(lng);
}, [lng]);

Синхронизация роутинга и i18next

При изменении локали необходимо обновлять URL.

navigate(`/${lng}/dashboard`);

Такой подход улучшает:

  • SEO;
  • индексацию;
  • поддержку мультиязычных ссылок.

Fallback language

Если перевод отсутствует:

fallbackLng: 'en'

i18next автоматически использует резервный словарь.

Возможна цепочка:

fallbackLng: ['en', 'ru']

Проверка существования перевода

Метод:

i18n.exists('header.title');

Пример:

if (i18n.exists('error.network')) {
  console.log('translation exists');
}

Смена языка вне React-компонентов

Иногда локаль переключается из:

  • API-модулей;
  • middleware;
  • сервисов.

Пример:

import i18n from './i18n';

i18n.changeLanguage('en');

Обработка Promise при смене языка

changeLanguage возвращает Promise.

i18n.changeLanguage('de')
  .then(() => {
    console.log('language changed');
  });

С async/await:

await i18n.changeLanguage('fr');

Это особенно важно при:

  • загрузке namespace;
  • SSR;
  • асинхронной инициализации.

Предзагрузка языков

Метод:

i18n.loadLanguages(['de', 'fr']);

Позволяет заранее загрузить переводы.

Преимущества:

  • мгновенное переключение;
  • отсутствие задержек;
  • улучшение UX.

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

В SSR-приложениях язык определяется до рендера.

Пример для Node.js:

i18n.changeLanguage(req.language);

Особенно важно в:

  • Next.js;
  • Remix;
  • Express SSR.

Интеграция с Next.js

Для Next.js часто используется:

  • next-i18next

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

module.exports = {
  i18n: {
    locales: ['en', 'ru'],
    defaultLocale: 'ru'
  }
};

Переключение:

router.push(router.pathname,
  router.asPath,
  { locale: 'en' }
);

Производительность при переключении языков

Основные проблемы:

  • повторные рендеры;
  • большие JSON-файлы;
  • задержки загрузки;
  • дублирование переводов.

Оптимизации:

  • namespaces;
  • lazy loading;
  • кэширование;
  • code splitting.

Ошибки при переключении языка

Неверный namespace

useTranslation('auth');

при отсутствии auth.json вызовет ошибки загрузки.


Отсутствующий ключ

t('profile.name')

Если ключ отсутствует, по умолчанию отображается сам ключ.


Несовпадение кодов языка

Ошибка:

changeLanguage('EN');

Правильно:

changeLanguage('en');

Коды ISO обычно записываются в нижнем регистре.


Debug-режим

Включение логирования:

i18n.init({
  debug: true
});

i18next начинает выводить:

  • загрузку переводов;
  • смену языка;
  • missing keys;
  • namespace events.

Типизация языков в TypeScript

Пример union type:

type Language =
  | 'ru'
  | 'en'
  | 'de';

Функция:

const changeLanguage = (
  lng: Language
) => {
  i18n.changeLanguage(lng);
};

Типизация переводов

Можно создать строгую типизацию ключей:

type TranslationKeys =
  | 'header.title'
  | 'auth.login'
  | 'auth.logout';

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

const tKey = (
  key: TranslationKeys
) => t(key);

Это снижает количество ошибок в крупных проектах.


Практическая структура production-приложения

src/
 ├── i18n/
 │    ├── index.js
 │    ├── config.js
 │    ├── detector.js
 │    └── backend.js
 │
 ├── locales/
 │    ├── en/
 │    ├── ru/
 │    └── de/
 │
 ├── shared/
 │    └── ui/
 │
 └── features/
      ├── auth/
      ├── dashboard/
      └── profile/

Такое разделение облегчает:

  • масштабирование;
  • поддержку;
  • lazy loading;
  • независимые namespace;
  • командную разработку.