TanStack Start v1でファイルから翻訳を読み込む方法

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

問題

ユーザー向けの文字列をコンポーネントに直接ハードコーディングすると、コンテンツとコードが密結合になります。文字列が変更されたり、新しい言語が追加されるたびに、開発者はソースファイルを探して修正し、アプリケーションを再デプロイする必要があります。このアプローチでは、翻訳ワークフローがエンジニアリングサイクルに依存し、技術者以外のチームメンバーがコピーを更新できなくなります。サポートする言語の数が増えるにつれて、適切な文字列を選択するための条件分岐ロジックが扱いにくくなり、エラーが発生しやすくなります。その結果、イテレーションが遅くなり、メンテナンスコストが高くなり、本来別の場所に配置すべき翻訳可能なコンテンツでコードベースが乱雑になります。

解決策

すべての翻訳可能な文字列をアプリケーションコードから分離し、ロケールごとに1つの外部JSONファイルに保存します。各メッセージに安定したキーを定義し、リテラルテキストの代わりにコンポーネント内でそれらのキーを参照します。実行時に、ユーザーのロケールに基づいて適切な翻訳ファイルを読み込み、それらのメッセージをreact-intlのIntlProviderに提供します。これによりコンテンツとコードが分離されます。翻訳者はJSONファイルを直接操作でき、コピーの変更にコード修正は不要になり、新しい言語の追加はコンポーネントに触れることなく新しいファイルを追加するだけで済みます。

手順

1. 各ロケール用の翻訳ファイルを作成する

翻訳を専用ディレクトリ内のJSONファイルとして整理し、すべてのメッセージのキーと値のペアを含むロケールごとに1つのファイルを作成します。

{
"welcome": "Welcome back",
"greeting": "Hello, {name}",
"itemCount": "{count, plural, =0 {No items} one {One item} other {# items}}"
}

これをapp/translations/en.jsonとして保存します。app/translations/es.jsonapp/translations/fr.jsonなど、他のロケール用の並列ファイルを作成し、同じキーで翻訳された値を使用します。

2. サーバー関数を使用して翻訳を読み込む

サーバー関数を使用して、リクエストされたロケールに基づいてディスクから翻訳ファイルを読み取り、SSR中はサーバー側で、ナビゲーション中はクライアント側で翻訳が読み込まれるようにします。

import { createServerFn } from "@tanstack/react-start";
import * as fs from "node:fs";

export const getMessages = createServerFn({ method: "GET" }).handler(
  async ({ request }) => {
    const url = new URL(request.url);
    const locale = url.searchParams.get("locale") || "en";
    const filePath = `app/translations/${locale}.json`;
    const content = await fs.promises.readFile(filePath, "utf-8");
    return JSON.parse(content);
  },
);

この関数は、指定されたロケールのJSONファイルを読み取り、解析されたメッセージオブジェクトを返します。サーバー上でのみ実行され、ファイルシステムへのアクセスを安全に保ちます。

3. ユーザーのロケールを判定するヘルパーを作成する

リクエストからロケールを抽出するか、デフォルトにフォールバックする小さなユーティリティを定義し、ルート間で再利用可能にします。

export function getLocaleFromRequest(request: Request): string {
  const url = new URL(request.url);
  const localeParam = url.searchParams.get("locale");
  if (localeParam) return localeParam;
  const acceptLanguage = request.headers.get("accept-language");
  if (acceptLanguage) {
    const match = acceptLanguage.split(",")[0].split("-")[0];
    return match || "en";
  }
  return "en";
}

この関数は、まずクエリパラメータをチェックし、次にAccept-Languageヘッダーをチェックして、英語をデフォルトとします。ロケール検出の単一の情報源を提供します。

4. ルートローダーでメッセージを読み込む

ルートローダーを使用して、レンダリング前に現在のロケールのメッセージを取得し、コンポーネントツリーで利用できるようにします。

import { createFileRoute } from "@tanstack/react-router";
import { getMessages, getLocaleFromRequest } from "../lib/i18n";

export const Route = createFileRoute("/")({
  loader: async ({ context }) => {
    const locale = getLocaleFromRequest(context.request);
    const messages = await getMessages({ data: { locale } });
    return { locale, messages };
  },
  component: HomePage,
});

function HomePage() {
  const { locale, messages } = Route.useLoaderData();
  return (
    <div>
      <p>{messages.welcome}</p>
    </div>
  );
}

ローダーはサーバー関数を呼び出してメッセージを取得し、コンポーネントはuseLoaderDataを介してそれらにアクセスします。このパターンは、SSRとクライアント側ナビゲーションの両方で機能します。

5. react-intlにメッセージを提供する

コンポーネントツリーをIntlProviderでラップし、読み込まれたロケールとメッセージを渡すことで、すべての子孫コンポーネントが翻訳にアクセスできるようにします。

import { IntlProvider } from "react-intl";

function HomePage() {
  const { locale, messages } = Route.useLoaderData();
  return (
    <IntlProvider locale={locale} messages={messages}>
      <AppContent />
    </IntlProvider>
  );
}

function AppContent() {
  return (
    <div>
      <FormattedMessage id="welcome" />
    </div>
  );
}

IntlProviderは、すべてのreact-intlコンポーネントとフックでロケールとメッセージを利用可能にします。コンポーネントは、FormattedMessageまたはuseIntlを使用してキーでメッセージを参照でき、読み込まれたロケールに基づいて正しい翻訳がレンダリングされます。

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

react-intlのFormattedMessageコンポーネントまたはuseIntlフックを使用して、JSONファイルで定義されたキーを参照し、翻訳された文字列を表示します。

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

function UserGreeting({ name }: { name: string }) {
  const intl = useIntl();
  const title = intl.formatMessage({ id: "greeting" }, { name });
  return (
    <div>
      <h1 title={title}>
        <FormattedMessage id="greeting" values={{ name }} />
      </h1>
      <p>
        <FormattedMessage id="itemCount" values={{ count: 5 }} />
      </p>
    </div>
  );
}

FormattedMessageは翻訳された文字列をインラインでレンダリングし、useIntl().formatMessageは属性やJavaScriptロジックで使用するための文字列を返します。どちらも補間用のvaluesを受け入れ、複数形や書式設定のためのICUメッセージ構文をサポートしています。