Next.js(Pages Router)v16でファイルから翻訳データを読み込む方法

翻訳対象の文言をコードから分離しよう

問題

ユーザー向けのテキストをコンポーネント内に直接ハードコードすると、コンテンツとコードが強く結び付いてしまいます。文言を変更するたびに、開発者が実装ファイルを探して修正する必要があります。2言語目を追加する時は、すべてのハードコードされたテキストを見つけて条件分岐で囲まなければなりません。3言語目になるとその論理がさらに複雑になります。こうした方法では翻訳ワークフローがコードのデプロイに依存するため、非技術メンバーが独自にコンテンツを更新することができません。

アプリケーションが大きくなるにつれ、翻訳管理はどんどん難しくなります。多くのコンポーネントに散らばるテキストは一括で確認・更新や重複管理がしにくいです。翻訳者はコードベースへのアクセスが必要なため、開発者と並行して作業することもできません。

解決策

ユーザー向けのテキストをすべて外部のリソースファイルにまとめ、言語ごとに1つのJSONファイルに整理します。各文言は原文ではなく固有のキーで管理し、コンポーネントはハードコードするのではなくそのキーを参照します。

Next.jsから受け取ったロケール情報に基づいて、該当言語の翻訳ファイルを読み込み、そのメッセージをreact-intlのプロバイダーに渡します。言語を切り替えたい場合も、ファイルを差し替えるだけでコンポーネントのコードを変更せずに対応できます。これによりコンテンツと実装が分離され、翻訳者は通常のJSONファイルで編集でき、開発者は安定したメッセージキーを参照できるようになります。

手順

1. ロケールごとに翻訳ファイルを作成する

翻訳テキストをロケールごとに分けてJSONファイルに保存しましょう。各ファイルには、安定した識別子(キー)と、その言語向けに翻訳された値をペアで記述します。

{
"welcome": "Welcome back",
"greeting": "Hello, {name}",
"itemCount": "You have {count, plural, one {# item} other {# items}}"
}

サポートする各ロケールごとに 1 つずつファイルを作成します(例: messages/en.jsonmessages/es.jsonmessages/fr.json)。これらのファイルはプロジェクトのルートにある messages ディレクトリ内に配置してください。すべてのファイルで同じキー名を使うことで、react-intl がアクティブなロケールに合わせて正しい翻訳を取得できます。

2. getStaticProps でメッセージを読み込む

getStaticProps で Next.js から渡されるロケール情報に基づいて翻訳ファイルを読み込みます。これにより、メッセージがサーバーサイドで利用可能となり、プロパティとしてページに渡されます。

import { GetStaticProps } from "next";

export const getStaticProps: GetStaticProps = async (context) => {
  const locale = context.locale || "en";
  const messages = (await import(`../messages/${locale}.json`)).default;

  return {
    props: {
      messages,
    },
  };
};

動的インポートにより、現在のロケール用ファイルだけが読み込まれます。Next.js は URL やユーザーの設定に応じて自動で locale の値を用意します。

3. _app で IntlProvider にメッセージを渡す

ルートコンポーネントを IntlProvider でラップし、ユーザーの現在のロケールと対応する翻訳済みメッセージで設定します。各ページから自分用のメッセージを提供できるように、pageProps からメッセージを取得してください。

import { AppProps } from "next/app";
import { IntlProvider } from "react-intl";
import { useRouter } from "next/router";

export default function App({ Component, pageProps }: AppProps) {
  const { locale, defaultLocale } = useRouter();

  return (
    <IntlProvider
      locale={locale || "en"}
      defaultLocale={defaultLocale || "en"}
      messages={pageProps.messages}
    >
      <Component {...pageProps} />
    </IntlProvider>
  );
}

プロバイダーによって、ツリー内のすべてのコンポーネントでメッセージが利用できるようになります。各ページは getStaticProps で独自のメッセージファイルを読み込み、_app はそのメッセージを pageProps 経由で受け取ります。

4. コンポーネント内でキーによってメッセージを参照する

react-intl の FormattedMessage コンポーネントや useIntl フックを利用して、翻訳テキストを表示します。文字列を直接埋め込むのではなく、必ずキーでメッセージを参照しましょう。

import { FormattedMessage, useIntl } from "react-intl";

export default function HomePage() {
  const intl = useIntl();
  const userName = "Alice";

  return (
    <div>
      <h1>
        <FormattedMessage id="welcome" />
      </h1>
      <p>
        <FormattedMessage id="greeting" values={{ name: userName }} />
      </p>
      <input placeholder={intl.formatMessage({ id: "searchPlaceholder" })} />
    </div>
  );
}

react-intl は指定した id の翻訳メッセージを検索して整形します。翻訳が見つからない場合、defaultMessage が指定されていればそれがフォールバックされます。values プロパティで渡した変数は、メッセージ文字列に埋め込まれます。