如何在 React Router v7 中支持从右到左(RTL)语言

为阿拉伯语和希伯来语镜像布局

问题

大多数设计系统默认文本从左到右流动。导航从左侧开始,侧边栏固定在左侧,内容也是从左到右阅读。但阿拉伯语和希伯来语是从右到左阅读的,它们的布局也应相应镜像——英文界面中出现在左侧的内容,在阿拉伯语界面中应出现在右侧。如果没有这种镜像,整个界面会显得颠倒。视觉流与阅读方向相悖,用户在阅读时不得不与布局“对抗”,体验非常不自然。

挑战不仅仅是文本对齐。边距、内边距、边框和定位都需要适配。例如,英文中按钮有左边距,阿拉伯语中应为右边距。指向右侧的图标应改为指向左侧。整个界面的空间逻辑都必须根据阅读方向进行翻转。

解决方案

在文档的 <html> 元素上设置 dir 属性,以指定当前语言的文本方向。对于英文等从左到右的语言,使用 ltr;对于阿拉伯语和希伯来语等从右到左的语言,使用 rtl。这个属性会让浏览器自动镜像许多布局行为。

使用 CSS 逻辑属性(如 margin-inline-start)设计布局,而不是物理属性(如 margin-left),这样在文本方向变化时,间距会自动适配。逻辑属性与方向无关——它们定义的是相对于文本流的间距,而不是固定的屏幕位置。当文档方向为 RTL 时,margin-inline-start 会变为 margin-right,布局会自动镜像,无需额外代码。

步骤

1. 检测当前 locale 的文本方向

React Router 7 要求在 app/root.tsx 处设置一个根路由,用于渲染 HTML 文档。请创建一个辅助函数,将 locale 代码映射到其文本方向。

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";
}

该函数会为每个支持的 locale 返回合适的文本方向,未知 locale 默认使用从左到右。

2. 设置 html 元素的 dir 属性

在根布局中,获取当前 locale,并将对应的文本方向应用到文档上。

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" 替换为基于 locale 检测机制的动态值。如果应用将当前 locale 存储在 loader 或 context 中,请在此读取并传递给 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 模式下水平翻转箭头图标,同时通过逻辑属性保持文本和间距的方向感知。并非所有图标都需要翻转——只需翻转那些指示方向或移动的图标。