Как создать компонент переключателя языков в React Router v7

Переключение языков, оставаясь на той же странице

Проблема

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

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

Решение

Создайте компонент переключателя языка, который считывает текущий URL и извлекает как активный параметр локали, так и оставшиеся сегменты пути. Для каждого поддерживаемого языка создайте новый URL, заменяя только сегмент локали, сохраняя при этом все остальные сегменты пути и параметры запроса. Отобразите эти URL-адреса в виде ссылок, чтобы пользователи могли переключать языки, не теряя своего положения в приложении.

Этот подход рассматривает локаль как заменяемый параметр в структуре URL, а не как пункт назначения навигации, гарантируя, что переключение с /en/products/shoes на /es/products/shoes сохраняет контекст пользователя.

Шаги

1. Создайте вспомогательную функцию для построения URL с учетом локали

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

export function buildLocalePath(
  currentPath: string,
  newLocale: string,
): string {
  const segments = currentPath.split("/").filter(Boolean);

  if (segments.length === 0) {
    return `/${newLocale}`;
  }

  segments[0] = newLocale;
  return `/${segments.join("/")}`;
}

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

2. Определите поддерживаемые локали

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

export const locales = [
  { code: "en", label: "English" },
  { code: "es", label: "Español" },
  { code: "fr", label: "Français" },
  { code: "de", label: "Deutsch" },
];

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

3. Создайте компонент переключателя языков

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

import { Link, useLocation, useParams } from "react-router";
import { locales, buildLocalePath } from "./i18n-config";

export function LanguageSwitcher() {
  const location = useLocation();
  const params = useParams();
  const currentLocale = params.locale || "en";

  return (
    <nav aria-label="Переключатель языков">
      <ul>
        {locales.map((locale) => {
          const isActive = locale.code === currentLocale;
          const newPath = buildLocalePath(location.pathname, locale.code);

          return (
            <li key={locale.code}>
              {isActive ? (
                <span aria-current="true">{locale.label}</span>
              ) : (
                <Link to={newPath}>{locale.label}</Link>
              )}
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

Компонент использует useLocation для доступа к текущему пути и useParams для извлечения активной локали из URL. Для каждой поддерживаемой локали он генерирует новый путь с помощью вспомогательной функции и отображает либо ссылку, либо неинтерактивный элемент для текущего языка.

4. Сохраните параметры запроса и фрагменты хэша

Расширьте вспомогательную функцию, чтобы сохранять строки запроса и фрагменты URL при переключении языков.

export function buildLocalePath(
  currentPath: string,
  search: string,
  hash: string,
  newLocale: string,
): string {
  const segments = currentPath.split("/").filter(Boolean);

  if (segments.length === 0) {
    return `/${newLocale}${search}${hash}`;
  }

  segments[0] = newLocale;
  return `/${segments.join("/")}${search}${hash}`;
}

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

5. Обновите компонент для использования улучшенного помощника

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

import { Link, useLocation, useParams } from "react-router";
import { locales, buildLocalePath } from "./i18n-config";

export function LanguageSwitcher() {
  const location = useLocation();
  const params = useParams();
  const currentLocale = params.locale || "en";

  return (
    <nav aria-label="Переключатель языков">
      <ul>
        {locales.map((locale) => {
          const isActive = locale.code === currentLocale;
          const newPath = buildLocalePath(
            location.pathname,
            location.search,
            location.hash,
            locale.code,
          );

          return (
            <li key={locale.code}>
              {isActive ? (
                <span aria-current="true">{locale.label}</span>
              ) : (
                <Link to={newPath}>{locale.label}</Link>
              )}
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

Теперь компонент передает location.search и location.hash во вспомогательную функцию, гарантируя, что URL-адреса, такие как /en/products?category=shoes#reviews, становятся /es/products?category=shoes#reviews при переключении на испанский.