Как поддерживать языки с письмом справа налево (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, при этом текст и отступы остаются зависимыми от направления с помощью логических свойств. Не все иконки нужно переворачивать — только те, что указывают направление или движение.