Как поддерживать языки с письмом справа налево (RTL) в React Router v7

Зеркальные макеты для арабского и иврита

Проблема

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

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

Решение

Установите атрибут dir на элемент <html> документа, чтобы указать направление текста для текущего языка. Используйте ltr для языков с письмом слева направо, таких как английский, и rtl для языков с письмом справа налево, таких как арабский и иврит. Этот единственный атрибут позволяет браузерам автоматически зеркалить многие элементы макета.

Проектируйте макеты с помощью CSS-логических свойств, таких как margin-inline-start, вместо физических свойств, таких как margin-left, чтобы отступы автоматически адаптировались при изменении направления текста. Логические свойства не зависят от направления — они определяют отступы относительно потока текста, а не фиксированных позиций на экране. Когда направление документа становится RTL, margin-inline-start превращается в margin-right, и макет зеркалится без дополнительного кода.

Шаги

1. Определите направление текста текущей локали

React Router 7 требует корневой маршрут на app/root.tsx, который рендерит HTML-документ. Создайте вспомогательную функцию, сопоставляющую коды локалей с их направлением текста.

const locales = {
  en: { dir: "ltr" },
  ar: { dir: "rtl" },
  he: { dir: "rtl" },
  es: { dir: "ltr" },
};

function getTextDirection(locale: string): "ltr" | "rtl" {
  return locales[locale as keyof typeof locales]?.dir || "ltr";
}

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

2. Установите атрибут dir на элементе html

В корневом макете получите текущую локаль и примените соответствующее направление к документу.

import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" dir="ltr">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function Root() {
  return <Outlet />;
}

Замените жестко заданное значение dir="ltr" на динамическое, основанное на вашей системе определения локали. Если ваше приложение хранит текущую локаль в загрузчике или контексте, получите её здесь и передайте в getTextDirection.

3. Используйте логические свойства для отступов

Замените физические CSS-свойства их логическими аналогами в компонентах и стилях.

export default function Card({
  title,
  children,
}: {
  title: string;
  children: React.ReactNode;
}) {
  return (
    <div
      style={{
        paddingInlineStart: "1rem",
        paddingInlineEnd: "1rem",
        marginInlineStart: "auto",
        borderInlineStart: "4px solid blue",
      }}
    >
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Когда dir="rtl" установлен, paddingInlineStart применяется к правой стороне, а paddingInlineEnd — к левой, автоматически зеркалируя макет. Один и тот же компонент корректно работает и в LTR, и в RTL-контекстах без дополнительной логики.

4. Применяйте логические свойства к контейнерам макета

Используйте логические свойства для типовых макетов, таких как навигационные панели и сетки контента.

export default function Navigation() {
  return (
    <nav
      style={{
        display: "flex",
        gap: "1rem",
        paddingInline: "2rem",
        borderBlockEnd: "1px solid #ccc",
      }}
    >
      <a href="/" style={{ marginInlineEnd: "auto" }}>
        Home
      </a>
      <a href="/about">About</a>
      <a href="/contact">Contact</a>
    </nav>
  );
}

paddingInline задаёт отступы по обеим инлайн-сторонам, а marginInlineEnd: "auto" сдвигает контент к inline-start, который меняется с левого на правый при смене направления. Навигация автоматически зеркалируется для RTL-языков.

5. Обрабатывайте иконки и направленную графику

Для иконок, обозначающих направление или поток, при необходимости меняйте их направление в зависимости от направления текста.

function BackButton() {
  const dir = document.documentElement.dir;
  const iconStyle = {
    transform: dir === "rtl" ? "scaleX(-1)" : "none",
    marginInlineEnd: "0.5rem",
  };

  return (
    <button>
      <span style={iconStyle}>←</span>
      Back
    </button>
  );
}

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