Next.js(ページルーター)v16でファイルから翻訳をロードする方法

翻訳可能なコンテンツをコードから分離する

問題点

ユーザー向けの文字列をコンポーネントに直接ハードコーディングすると、コンテンツとコードの間に強い結合が生じます。テキストが変更されるたびに、開発者は実装ファイルを探して修正する必要があります。第二言語のサポートを追加するには、すべてのハードコードされた文字列を見つけて条件付きロジックでラップする必要があります。第三言語を追加するとそのロジックがさらに複雑になります。このアプローチでは、翻訳ワークフローがコードデプロイメントに依存し、非技術系チームメンバーが独自にコンテンツを更新することができなくなります。

アプリケーションが成長するにつれて、翻訳の管理はますます困難になります。数十のコンポーネントに散らばった文字列は、監査、複製、一貫した更新が難しくなります。翻訳者はコードベース自体にアクセスする必要があるため、開発者と並行して作業することができません。

解決策

すべてのユーザー向けテキストを外部リソースファイルに保存し、言語ごとに整理して、ロケールごとに1つのJSONファイルを用意します。各メッセージは、リテラルテキストではなく一意のキーで識別されます。コンポーネントはハードコードされた文字列の代わりにこれらのキーを参照します。

Next.jsから受け取ったロケールに基づいて適切な翻訳ファイルをロードし、メッセージをreact-intlのプロバイダーに渡します。これにより、アプリケーションはコンポーネントコードを変更することなく、異なるファイルをロードすることで言語を切り替えることができます。これによりコンテンツと実装が分離され、翻訳者は標準のJSONファイルで作業し、開発者は安定したメッセージキーを参照できるようになります。

ステップ

1. ロケールごとに整理された翻訳ファイルを作成する

翻訳メッセージをロケールごとに分けたJSONファイルに配置します。各ファイルには、キーが安定した識別子であり、値がその言語の翻訳された文字列であるキーと値のペアが含まれています。

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

プロジェクトルートのmessagesディレクトリに、サポートする各ロケールごとに1つのファイル(例:messages/en.jsonmessages/es.jsonmessages/fr.json)を作成します。react-intlがアクティブなロケールに対して正しい翻訳を検索できるように、すべてのファイルで同じキーを使用します。

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

Next.jsからgetStaticPropsで受け取ったロケールに基づいて翻訳ファイルを読み込みます。これによりメッセージがサーバーサイドで利用可能になり、ページにpropsとして渡されます。

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を通じて独自のメッセージファイルを読み込み、_apppagePropsを通じてそれらのメッセージを受け取ります。

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プロップで渡された変数はメッセージ文字列に補間されます。