React Router v7で右から左(RTL)言語をサポートする方法

アラビア語とヘブライ語のレイアウトをミラーリングする

問題

ほとんどのデザインシステムは、テキストが左から右に流れることを前提としています。ナビゲーションは左から始まり、サイドバーは左に固定され、コンテンツは左から右に読まれます。しかし、アラビア語とヘブライ語は右から左に読まれるため、それに応じてレイアウトをミラーリングする必要があります。英語で左に表示されるものは、アラビア語では右に表示されるべきです。このミラーリングがないと、インターフェース全体が逆向きに感じられます。視覚的な流れが読む方向と矛盾し、ユーザーが自然に読むためにレイアウトと戦わなければならない、方向感覚を失わせる体験を生み出します。

課題は単純なテキストの配置を超えて広がります。マージン、パディング、ボーダー、配置のすべてが適応する必要があります。英語で左マージンを持つボタンは、アラビア語では右マージンを持つべきです。右を指すアイコンは左を指すべきです。インターフェースの空間的ロジック全体が、読む方向に合わせて反転する必要があります。

解決策

ドキュメントの<html>要素にdir属性を設定して、現在の言語のテキスト方向を指定します。英語のような左から右の言語にはltrを使用し、アラビア語やヘブライ語のような右から左の言語にはrtlを使用します。この単一の属性により、ブラウザは多くのレイアウト動作を自動的にミラーリングします。

margin-leftのような物理的プロパティの代わりに、margin-inline-startのようなCSS論理プロパティを使用してレイアウトを設計すると、テキスト方向が変わったときにスペーシングが自動的に適応します。論理プロパティは方向に依存しません。固定された画面位置ではなく、テキストの流れに対して相対的にスペーシングを定義します。ドキュメントの方向がRTLの場合、margin-inline-startmargin-rightになり、追加のコードなしでレイアウトが自動的にミラーリングされます。

手順

1. 現在のロケールのテキスト方向を検出する

React Router 7では、HTMLドキュメントをレンダリングするapp/root.tsxにルートルートが必要です。ロケールコードをテキスト方向にマッピングするヘルパー関数を作成します。

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. html要素にdir属性を設定する

ルートレイアウトで、現在のロケールを取得し、対応する方向をドキュメントに適用します。

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"はコンテンツをインライン開始エッジにプッシュします。これは方向が変わると左から右に反転します。ナビゲーションレイアウトは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モードで矢印アイコンが水平方向に反転され、論理プロパティを通じてテキストと間隔が方向を認識したまま保たれます。すべてのアイコンを反転させる必要はありません。方向や動きを示すアイコンのみが対象です。