Documentation Next.js

はじめに

グローバル向けのWebアプリケーションを開発する際、RTL(Right-to-Left)言語への対応は避けて通れない課題です。アラビア語、ヘブライ語、ペルシャ語、ウルドゥー語など、世界には右から左に読む言語を使用する数億人のユーザーが存在します。

この記事では、Next.jsアプリケーションでRTL言語に対応するための具体的な実装方法を解説します。next-i18nextによる多言語対応、Tailwind CSSを活用したスタイリング、そして実践的なコンポーネント設計まで、包括的に紹介します。

RTL言語とは

**RTL(Right-to-Left)**とは、テキストを右から左に向かって読み書きする言語のことです。主なRTL言語には以下があります。

言語言語コード話者数(概算)
アラビア語ar約4億人
ヘブライ語he約900万人
ペルシャ語(ファルシ語)fa約1億人
ウルドゥー語ur約2億人

これらの言語に対応することで、より多くのユーザーにサービスを届けることができます。

RTL対応の基本設定

dir属性の動的設定

RTL対応の基本は、HTMLのdir(direction)属性を使ってテキストの方向を制御することです。dir="rtl"を設定すると、ブラウザは自動的にテキストの配置やレイアウトを右から左に調整します。

// _app.js または _app.tsx
import { useRouter } from 'next/router';
import { useEffect } from 'react';

// RTL言語のリストを定義
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];

function MyApp({ Component, pageProps }) {
  const { locale } = useRouter();

  useEffect(() => {
    // 現在のロケールがRTL言語かどうかを判定
    const isRtl = RTL_LANGUAGES.includes(locale);
    const dir = isRtl ? 'rtl' : 'ltr';

    // html要素にdir属性を設定
    document.documentElement.setAttribute('dir', dir);
    // lang属性も同時に設定(SEOとアクセシビリティのため)
    document.documentElement.setAttribute('lang', locale);
  }, [locale]);

  return <Component {...pageProps} />;
}

export default MyApp;

このコードでは、ユーザーの選択した言語に応じてdir属性を動的に変更します。RTL_LANGUAGES配列にRTL言語のコードをまとめることで、メンテナンス性を高めています。

App Routerでの設定

Next.js 13以降のApp Routerを使用している場合は、layout.tsxで設定します。

// app/[locale]/layout.tsx
import { ReactNode } from 'react';

// RTL言語のリスト
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];

interface RootLayoutProps {
  children: ReactNode;
  params: { locale: string };
}

export default function RootLayout({
  children,
  params: { locale }
}: RootLayoutProps) {
  // 現在のロケールがRTL言語かどうかを判定
  const isRtl = RTL_LANGUAGES.includes(locale);
  const dir = isRtl ? 'rtl' : 'ltr';

  return (
    <html lang={locale} dir={dir}>
      <body>{children}</body>
    </html>
  );
}

next-i18nextを使った多言語対応

基本設定

next-i18nextは、Next.jsで国際化(i18n)を実装するための定番ライブラリです。まず、必要なパッケージをインストールします。

npm install next-i18next react-i18next i18next

次に、設定ファイルを作成します。

// next-i18next.config.js
module.exports = {
  i18n: {
    // 対応する言語のリスト
    locales: ['en', 'ar', 'he', 'ja'],
    // デフォルトの言語
    defaultLocale: 'en',
    // ロケール検出を有効化
    localeDetection: true,
  },
  // 翻訳ファイルの場所(デフォルト: public/locales)
  localePath: './public/locales',
};

翻訳ファイルの配置

翻訳ファイルはpublic/localesディレクトリに言語ごとに配置します。

public/
└── locales/
    ├── en/
    │   └── common.json
    ├── ar/
    │   └── common.json
    └── he/
        └── common.json
// public/locales/en/common.json
{
  "welcome": "Welcome",
  "navigation": {
    "home": "Home",
    "about": "About",
    "contact": "Contact"
  }
}
// public/locales/ar/common.json
{
  "welcome": "مرحبا",
  "navigation": {
    "home": "الرئيسية",
    "about": "حول",
    "contact": "اتصل"
  }
}

Next.js設定の更新

next.config.jsにi18n設定を追加します。

// next.config.js
const { i18n } = require('./next-i18next.config');

module.exports = {
  i18n,
  // その他の設定...
};

コンポーネントでの使用

翻訳を使用するコンポーネントの例です。

// components/Header.tsx
import { useTranslation } from 'next-i18next';
import Link from 'next/link';

export const Header = () => {
  // 'common'は翻訳ファイル名(common.json)
  const { t } = useTranslation('common');

  return (
    <header>
      <nav>
        {/* t()関数で翻訳テキストを取得 */}
        <Link href="/">{t('navigation.home')}</Link>
        <Link href="/about">{t('navigation.about')}</Link>
        <Link href="/contact">{t('navigation.contact')}</Link>
      </nav>
    </header>
  );
};

Tailwind CSSによるRTLサポート

RTL/LTR修飾子の使用

Tailwind CSS v3.3.0以降では、RTLサポートが組み込まれており、rtl:およびltr:修飾子でスタイルを簡単に切り替えられます。

<!-- RTL/LTR対応のナビゲーション -->
<nav class="flex">
  <!-- LTRでは左マージン、RTLでは右マージンを適用 -->
  <div class="ltr:ml-4 rtl:mr-4">
    ナビゲーションアイテム
  </div>

  <!-- テキスト配置の切り替え -->
  <p class="ltr:text-left rtl:text-right">
    この段落はLTRでは左揃え、RTLでは右揃えになります
  </p>
</nav>

論理プロパティの活用

より効率的なアプローチとして、**論理プロパティ(Logical Properties)**を使用する方法があります。論理プロパティは、物理的な方向(left/right)ではなく、書字方向に基づいた方向(start/end)でスタイルを指定します。

物理プロパティ論理プロパティ(Tailwind)説明
ml-* (margin-left)ms-* (margin-start)開始側のマージン
mr-* (margin-right)me-* (margin-end)終了側のマージン
pl-* (padding-left)ps-* (padding-start)開始側のパディング
pr-* (padding-right)pe-* (padding-end)終了側のパディング
left-*start-*開始位置
right-*end-*終了位置
<!-- 論理プロパティを使用した例 -->
<div class="flex">
  <!-- ms-4: LTRではmargin-left、RTLではmargin-right -->
  <button class="ms-4 ps-6 pe-6">
    送信
  </button>
</div>

<!-- アイコンの配置 -->
<div class="flex items-center">
  <span class="me-2">📧</span>
  <span>メールアドレス</span>
</div>

RTL対応コンポーネントの実装例

実践的なRTL対応カードコンポーネントの例です。

// components/Card.tsx
interface CardProps {
  title: string;
  description: string;
  icon: React.ReactNode;
}

export const Card = ({ title, description, icon }: CardProps) => {
  return (
    <div className="flex items-start p-4 border rounded-lg">
      {/* アイコン: 論理プロパティでマージンを設定 */}
      <div className="flex-shrink-0 me-4">
        {icon}
      </div>

      {/* コンテンツ */}
      <div className="flex-1">
        {/* テキスト配置は自動的にdirに従う */}
        <h3 className="text-lg font-bold">{title}</h3>
        <p className="text-gray-600">{description}</p>
      </div>

      {/* 矢印アイコン: RTLでは反転 */}
      <div className="flex-shrink-0 ms-4 rtl:rotate-180">
        <svg className="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
          <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
        </svg>
      </div>
    </div>
  );
};

ロケール切り替え機能

言語切り替えコンポーネント

ユーザーが言語を切り替えられるコンポーネントを実装します。

// components/LocaleSwitcher.tsx
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';

// 言語の表示名マッピング
const LANGUAGE_NAMES: Record<string, string> = {
  en: 'English',
  ar: 'العربية',
  he: 'עברית',
  ja: '日本語',
};

export const LocaleSwitcher = () => {
  const router = useRouter();
  const { locale, locales, asPath } = router;
  const { t } = useTranslation('common');

  // 言語切り替え処理
  const handleLocaleChange = (newLocale: string) => {
    // 同じページを新しいロケールで表示
    router.push(asPath, asPath, { locale: newLocale });
  };

  return (
    <div className="flex gap-2">
      {locales?.map((loc) => (
        <button
          key={loc}
          onClick={() => handleLocaleChange(loc)}
          className={`
            px-3 py-1 rounded
            ${locale === loc
              ? 'bg-blue-600 text-white'
              : 'bg-gray-200 hover:bg-gray-300'}
          `}
          // アクセシビリティ: 現在選択中の言語を示す
          aria-current={locale === loc ? 'true' : undefined}
        >
          {LANGUAGE_NAMES[loc] || loc}
        </button>
      ))}
    </div>
  );
};

ドロップダウン形式の言語切り替え

多くの言語をサポートする場合は、ドロップダウン形式が適切です。

// components/LocaleDropdown.tsx
import { useRouter } from 'next/router';
import { useState } from 'react';

const LANGUAGE_NAMES: Record<string, string> = {
  en: 'English',
  ar: 'العربية',
  he: 'עברית',
  ja: '日本語',
};

export const LocaleDropdown = () => {
  const router = useRouter();
  const { locale, locales, asPath } = router;
  const [isOpen, setIsOpen] = useState(false);

  const handleLocaleChange = (newLocale: string) => {
    router.push(asPath, asPath, { locale: newLocale });
    setIsOpen(false);
  };

  return (
    <div className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center gap-2 px-4 py-2 border rounded"
        aria-expanded={isOpen}
        aria-haspopup="listbox"
      >
        <span>🌐</span>
        <span>{LANGUAGE_NAMES[locale || 'en']}</span>
        <span className={`transition-transform ${isOpen ? 'rotate-180' : ''}`}>

        </span>
      </button>

      {isOpen && (
        <ul
          className="absolute top-full mt-1 w-full bg-white border rounded shadow-lg z-10"
          role="listbox"
        >
          {locales?.map((loc) => (
            <li key={loc}>
              <button
                onClick={() => handleLocaleChange(loc)}
                className={`
                  w-full px-4 py-2 text-start hover:bg-gray-100
                  ${locale === loc ? 'bg-blue-50' : ''}
                `}
                role="option"
                aria-selected={locale === loc}
              >
                {LANGUAGE_NAMES[loc] || loc}
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

RTL対応のベストプラクティス

1. 論理プロパティを優先する

物理的な方向(left/right)ではなく、論理的な方向(start/end)を使用することで、コードの重複を避けられます。

/* 避けるべきパターン */
.button {
  margin-left: 1rem;
}
[dir="rtl"] .button {
  margin-left: 0;
  margin-right: 1rem;
}

/* 推奨パターン */
.button {
  margin-inline-start: 1rem;
}

2. アイコンの方向に注意する

矢印やチェックマークなど、方向を示すアイコンはRTLで反転が必要な場合があります。

// RTLで反転が必要なアイコン
<span className="rtl:scale-x-[-1]">→</span>

// 反転不要なアイコン(対称的なもの)
<span>✓</span>

3. テキストの混在に対応する

RTLテキスト内にLTR(英語やURLなど)が含まれる場合、unicode-bidiプロパティを適切に設定します。

<!-- 数字やURLが含まれる場合 -->
<p dir="rtl">
  価格: <span dir="ltr">$99.99</span>
</p>

4. テスト環境を整える

RTL言語が読めなくても、動作確認は必要です。ブラウザの開発者ツールでdir属性を切り替えてテストできます。

// コンソールでRTL/LTRを切り替え
document.documentElement.dir = 'rtl'; // RTLモード
document.documentElement.dir = 'ltr'; // LTRモード

まとめ

Next.jsでのRTL対応は、以下のポイントを押さえることで効率的に実装できます。

  1. dir属性の動的設定: ロケールに応じてhtml要素のdir属性を切り替える
  2. next-i18nextの活用: 多言語対応の基盤として翻訳管理を行う
  3. 論理プロパティの使用: Tailwind CSSのms-*/me-*などを活用し、RTL/LTR両対応のスタイルを効率的に記述
  4. コンポーネント設計: RTLを意識したコンポーネント設計で保守性を高める

適切なRTL対応により、アラビア語圏やヘブライ語圏のユーザーにも快適なUXを提供でき、グローバル展開の可能性が広がります。

参考文献

円