翻訳の読み込み

メッセージを管理するためのプロバイダーの使用

問題

'Hello World'のようなテキストをアプリケーションのコンポーネントに直接ハードコーディングすると、コンテンツとコードが密結合してしまいます。異なる言語を表示するには、開発者はコンポーネントを複製するか、if/elseロジックを追加する必要があり、翻訳が拡張できなくなり、新しいテキストごとにコード全体の変更が必要になります。

解決策

react-intlIntlProviderを使用して翻訳を提供します。サーバー上のルートレイアウトで翻訳メッセージを読み込み、それをクライアント側のプロバイダーコンポーネントに渡し、他のクライアントコンポーネントでuseIntlフックを使用して消費します。

ステップ

1. react-intlをインストールする

まず、プロジェクトにreact-intlを依存関係として追加します。

npm install react-intl

2. フラットな翻訳ファイルを作成する

dictionariesフォルダを作成します。その中に、各言語のJSONファイルを追加します。react-intlはフラットなキーバリュー構造で最もよく機能します。

// dictionaries/en.json
{
  "home.title": "Home Page",
  "home.welcome": "Hello, welcome to our site!",
  "about.title": "About Us"
}
// dictionaries/es.json
{
  "home.title": "Página de Inicio",
  "home.welcome": "¡Hola, bienvenido a nuestro sitio!",
  "about.title": "Sobre Nosotros"
}

3. 辞書を読み込む関数を作成する

langパラメータに基づいてサーバー上で正しい辞書ファイルを読み込むヘルパー関数を作成します。

// app/get-dictionary.ts
import 'server-only';

// フラットなメッセージオブジェクトの型を定義
type Messages = Record<string, string>;

const dictionaries: { [key: string]: () => Promise<Messages> } = {
  en: () => import('@/dictionaries/en.json').then((module) => module.default),
  es: () => import('@/dictionaries/es.json').then((module) => module.default),
  // fr: () => import('@/dictionaries/fr.json').then((module) => module.default),
};

export const getDictionary = async (lang: string) => {
  const load = dictionaries[lang];
  if (load) {
    return load();
  }
  // 英語にフォールバック
  return dictionaries.en();
};

4. クライアントサイドプロバイダーを作成する

IntlProviderはReactのContextを使用するクライアントコンポーネントです。サーバーからロードされたメッセージを受け取ることができるラッパーを作成する必要があります。

// app/components/IntlClientProvider.tsx
'use client';

import { IntlProvider } from 'react-intl';

type Props = {
  children: React.ReactNode;
  locale: string;
  messages: Record<string, string>; // フラットなメッセージオブジェクト
};

export default function IntlClientProvider({
  children,
  locale,
  messages,
}: Props) {
  return (
    <IntlProvider messages={messages} locale={locale} defaultLocale="en">
      {children}
    </IntlProvider>
  );
}

5. ルートレイアウトを更新する

app/[lang]/layout.tsxasyncコンポーネントに変更します。これによりメッセージをロードし、IntlClientProviderに渡します。

// app/[lang]/layout.tsx
import { getDictionary } from '@/app/get-dictionary';
import IntlClientProvider from '@/app/components/IntlClientProvider';

export async function generateStaticParams() {
  return [{ lang: 'en' }, { lang: 'es' }];
}

export default async function RootLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { lang: string };
}) {
  // サーバー上でメッセージをロード
  const messages = await getDictionary(params.lang);

  return (
    <html lang={params.lang}>
      <body>
        {/* メッセージをクライアントプロバイダーに渡す */}
        <IntlClientProvider locale={params.lang} messages={messages}>
          {children}
        </IntlClientProvider>
      </body>
    </html>
  );
}

6. クライアントコンポーネントで翻訳を使用する

これで任意のクライアントコンポーネントでuseIntlフックを使用できます。サーバーコンポーネントではこのフックを使用できません。

翻訳されたテキストを表示する新しいクライアントコンポーネントを作成します:

// app/components/HomePageContent.tsx
'use client';

import { useIntl } from 'react-intl';

export default function HomePageContent() {
  const intl = useIntl();

  return (
    <div>
      <h1>{intl.formatMessage({ id: 'home.title' })}</h1>
      <p>{intl.formatMessage({ id: 'home.welcome' })}</p>
    </div>
  );
}

7. コンポーネントをページに追加する

最後に、新しいクライアントコンポーネントをページに追加します。

// app/[lang]/page.tsx
import HomePageContent from '@/app/components/HomePageContent';

export default function Home() {
  // このページはサーバーコンポーネントです
  return (
    <div>
      {/* クライアントコンポーネントをレンダリングします */}
      <HomePageContent />
    </div>
  );
}