如何在 Next.js(Pages Router)v16 中支持从右到左(RTL)语言

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

问题

大多数网页布局默认采用从左到右的文本流。导航菜单通常固定在左侧,内容从左到右阅读,间距通常通过 margin-leftpadding-right 等属性设置。当应用程序被翻译为阿拉伯语或希伯来语时,这些语言是从右到左书写的,整个视觉布局应进行镜像以适应。如果不进行镜像,用户会遇到视觉层级与自然阅读方向相悖的界面,导致导航混乱,应用体验不佳。

挑战不仅仅在于文本对齐。像 leftrightmargin-leftpadding-right 这样的物理 CSS 属性是绑定在屏幕的固定位置,而不是内容流。当文本方向发生变化时,这些属性依然锚定在原有的物理边缘,导致布局无法自然适应。

解决方案

将文档的文本方向属性设置为当前语言环境,使浏览器能够自动反转 RTL 语言的布局流。用基于内容流的逻辑属性替换物理 CSS 属性。像 margin-inline-startpadding-inline-end 这样的逻辑属性会根据文本方向自动映射到正确的物理边缘,使布局无需额外代码或条件样式即可自动镜像。

这种方法可以创建与方向无关的布局,无需重复样式或增加复杂逻辑,即可同时支持 LTR 和 RTL 语言。

步骤

1. 创建一个根据语言环境判断文本方向的辅助函数

逻辑属性会根据文本方向自动适配,其中 margin-inline-start 在 LTR(从左到右)环境下等同于 margin-left,在 RTL(从右到左)环境下等同于 margin-right。请编写一个实用函数,将 locale 映射到其文本方向。

export function getDirection(locale: string): "ltr" | "rtl" {
  const rtlLocales = ["ar", "he", "fa", "ur"];
  return rtlLocales.includes(locale) ? "rtl" : "ltr";
}

该函数用于识别 RTL 语言,并返回适用于 HTML 和 CSS 的正确方向值。

2. 设置文档方向属性

自定义 Document 类可以重写 render 方法,以便为 HTML 元素自定义 lang 和 dir 等属性。请创建一个自定义 Document,根据当前 locale 在根 HTML 元素上设置 dir 属性。

import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
} from "next/document";
import { getDirection } from "../utils/direction";

class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const initialProps = await Document.getInitialProps(ctx);
    return initialProps;
  }

  render() {
    const locale = this.props.__NEXT_DATA__.locale || "en";
    const dir = getDirection(locale);

    return (
      <Html lang={locale} dir={dir}>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

Next.js 路由器通过 locale 属性提供当前激活的 locale。文档会从 Next.js 路由数据中读取 locale,并将相应的方向应用到 HTML 元素,实现浏览器级的 RTL 支持。

3. 用逻辑属性替换物理 CSS 属性

逻辑属性如 margin-inline-start、padding-inline-start 及其简写形式 margin-inline 和 padding-inline,会根据书写模式和方向自动映射到物理属性。请将组件样式更新为使用流向相关的属性。

export default function Navigation() {
  return (
    <nav className="nav">
      <ul className="nav-list">
        <li className="nav-item">Home</li>
        <li className="nav-item">About</li>
        <li className="nav-item">Contact</li>
      </ul>
    </nav>
  );
}
.nav {
  padding-inline: 1rem;
  border-inline-start: 4px solid blue;
}

.nav-list {
  display: flex;
  gap: 1rem;
  margin-block: 0;
  padding-inline-start: 0;
}

.nav-item {
  margin-inline-end: 1.5rem;
}

margin、padding 和 inset 的逻辑属性让元素在不同书写模式下的定位更加简单高效。这些属性在 RTL 环境下会自动反转,无需额外样式或媒体查询。

4. 使用逻辑属性进行定位

对于绝对或相对定位的元素,请将 leftright 替换为 inset-inline-startinset-inline-end

export default function Sidebar() {
  return (
    <aside className="sidebar">
      <button className="close-button">×</button>
      <p>Sidebar content</p>
    </aside>
  );
}
.sidebar {
  position: fixed;
  inset-inline-start: 0;
  inset-block-start: 0;
  inline-size: 250px;
  block-size: 100vh;
  padding-inline: 1rem;
}

.close-button {
  position: absolute;
  inset-inline-end: 0.5rem;
  inset-block-start: 0.5rem;
}

在逻辑属性方法中,width 属性被 inline-size 替代,height 属性被 block-size 替代。这些属性确保定位元素能够根据当前文本方向显示在正确的位置。

5. 应用逻辑文本对齐

text-align: lefttext-align: right 替换为 text-align: starttext-align: end

.content {
  text-align: start;
}

.metadata {
  text-align: end;
  margin-inline-start: auto;
}

逻辑属性是指根据内容流动方向来描述盒子边缘,而不是物理视口尺寸。startend 这类文本对齐值会根据文本方向自动适配,使内容在 LTR 和 RTL 语言下都能正确对齐。