# General Translation Integrations: React Router で Shopify Hydrogen をローカライズする
URL: https://generaltranslation.com/ja/docs/integrations/shopify/guides/localizing-react-router.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: React Router 上の Hydrogen ストアフロントに gt-react を追加し、Shopify の言語コンテキストと同期させる方法。

React Router のストアフロントでは、`gt-react` がコード内のテキストを翻訳し、Hydrogen が Shopify との通信を担います。Shopify 固有の作業として必要なのは、Hydrogen のリクエストコンテキストにページと同じロケールを渡すことです。

*注: このガイドは、React Router 8 上の `@shopify/hydrogen` 2026.10、または `react-router.config.ts` で `future.v8_middleware: true` を設定した React Router 7 上の同バージョンを対象としています。*

<Callout type="info">
  **Hydrogen 2026.10 での変更点:** `createHydrogenContext` と React Router 専用のテンプレートは廃止され、フレームワーク非依存の `createShopifyRequestContext` に置き換えられました。従来と同じ `i18n` の言語と国に加え、任意で `pathPrefix` を受け取ります。アップグレードには、Hydrogen に同梱されている `migrate-to-hydrogen-2026-10` スキルを利用できます。
</Callout>

## 始める前に [#before-start]

* `npx @shopify/hydrogen setup` を実行して、React Router アプリに Hydrogen をセットアップします。ルートミドルウェアが Shopify のリクエストコンテキストと Storefront クライアントを作成し、`handleShopifyRoutes` を実行します。
* [React Router クイックスタート](/docs/react/react-router-quickstart)を完了しておいてください。ルートローダーがロケールと翻訳スナップショットを返し、ルートの `Layout` が [`GTProvider`](/docs/react/reference/components/gt-provider) と `<html lang>` をレンダリングする構成になっている必要があります。このガイドでは、クイックスタートで使用した [`<LocaleSelector>`](/docs/react/reference/components/locale-selector) を、URL を切り替える言語切り替え機能に置き換えます。

## URL からロケールを解決する [#request-locale]

ロケールごとに固有の URL を割り当てられるよう、リクエストのパスからロケールを読み取ります。`gt-react` と Hydrogen の両方で、共通のモジュールを 1 つ使用します：

```ts title="app/lib/i18n.ts"
import gtConfig from '../../gt.config.json';

// "/fr/products/x" -> "fr"。プレフィックスのないパスはデフォルトロケールを使用します。
export function getRequestLocale(request: Request): string {
  const [, segment] = new URL(request.url).pathname.split('/');
  return gtConfig.locales.includes(segment) ? segment : gtConfig.defaultLocale;
}

// "/products/x" -> "/fr/products/x"。デフォルトロケールではプレフィックスなしのパスのままにします。
export function localizePath(pathname: string, locale: string): string {
  if (locale === gtConfig.defaultLocale) return pathname;
  return pathname === '/' ? `/${locale}` : `/${locale}${pathname}`;
}

// "/fr/products/x" -> "/products/x"。プレフィックスのないパスはそのまま返されます。
export function stripLocale(pathname: string): string {
  const [, segment, ...rest] = pathname.split('/');
  return gtConfig.locales.includes(segment) ? `/${rest.join('/')}` : pathname;
}
```

ルートローダーでは、`parseLocale(request)` の代わりに `getRequestLocale(request)` を呼び出し、その結果を [`getTranslationsSnapshot`](/docs/react/reference/functions/get-translations-snapshot) に渡します。

*注: `parseLocale` はまずロケールのクッキーを読み取り、次に `Accept-Language` を参照します。これを単独で使うと、すべての言語が 1 つの URL から配信されるため、検索エンジンにインデックスされる言語は 1 つだけになります。*

プレフィックス付きのパスを配信するには、ページを任意の `:locale?` レイアウトルートの下にネストします。一致しないパスも Hydrogen のリダイレクトに到達するよう、キャッチオールルートは最後に配置してください：

```ts title="app/routes.ts"
import {type RouteConfig, index, route} from '@react-router/dev/routes';

export default [
  // ページは、プレフィックスなし、または /fr のようなターゲットロケールのプレフィックス付きのパスでレンダリングされます。
  route(':locale?', 'routes/locale.tsx', [
    index('routes/home.tsx'),
    route('products/:handle', 'routes/product.tsx'),
  ]),
  route('*', 'routes/catchall.tsx'),
] satisfies RouteConfig;
```

レイアウトルートでは、セグメントが設定済みの対象ロケールでない場合に 404 を返すようにします。これにより、`/en/...` や未知のプレフィックスで同じページの重複コピーがレンダリングされるのを防げます：

```tsx title="app/routes/locale.tsx"
import {Outlet} from 'react-router';
import gtConfig from '../../gt.config.json';

// プレフィックスとして有効なのはターゲットロケールのみ。デフォルトロケールにはプレフィックスを付けない。
export async function loader({params}: {params: {locale?: string}}) {
  if (params.locale && !gtConfig.locales.includes(params.locale)) {
    throw new Response(null, {status: 404});
  }
  return null;
}

export default function LocaleLayout() {
  return <Outlet />;
}
```

ファイルベースのルートを使用する場合は、代わりにレイアウトの名前を `($locale).tsx` にし、その配下にページファイルをネストしてください。

`/api/cart`、`/api/predictive-search`、`/checkout`、Customer Account ルートなど、Hydrogen 独自のハンドラーは、ページがレンダリングされる前にルートミドルウェアで応答するため、プレフィックスなしのままになります。

### Hydrogen のハンドラーをページの言語に合わせる [#handler-locale]

`/api/cart` と `/api/predictive-search` へのリクエストにはロケールプレフィックスがないため、`getRequestLocale` はこれらをデフォルトロケールに解決します。その結果、フランス語のページでもカートや検索結果には英語のカタログテキストが表示されてしまいます。

ページのロケールをクッキーに保存し、プレフィックスのない `/api` リクエストに限ってそのクッキーを読み取るようにします。ページのロケールは、これまでどおり URL のみから決定します。`app/lib/i18n.ts` の `getRequestLocale` を次のように置き換えます。

```ts title="app/lib/i18n.ts"
export const LOCALE_COOKIE_NAME = 'shop_locale';

const allLocales = [gtConfig.defaultLocale, ...gtConfig.locales];

export function readCookie(request: Request, name: string): string | undefined {
  const cookie = request.headers.get('Cookie')?.split(/;\s*/).find((part) => part.startsWith(`${name}=`));
  return cookie?.slice(name.length + 1);
}

/** ページは URL のプレフィックスからロケールを決定し、Hydrogen の `/api` ハンドラーは呼び出し元ページのロケールを使用します。 */
export function getRequestLocale(request: Request): string {
  const {pathname} = new URL(request.url);
  const [, segment] = pathname.split('/');
  if (gtConfig.locales.includes(segment)) return segment;
  if (!pathname.startsWith('/api/')) return gtConfig.defaultLocale;
  const remembered = readCookie(request, LOCALE_COOKIE_NAME);
  // 設定済みのロケールのみを信頼し、それ以外はデフォルトにフォールバックします。
  return remembered && allLocales.includes(remembered) ? remembered : gtConfig.defaultLocale;
}

/** ページの読み込みとクライアント側のナビゲーションが対象です。`/api` 呼び出しや投機的なプリフェッチは対象外です。 */
export function isPageNavigation(request: Request): boolean {
  const purpose = `${request.headers.get('Purpose') ?? ''} ${request.headers.get('Sec-Purpose') ?? ''}`;
  return (
    request.method === 'GET' &&
    !new URL(request.url).pathname.startsWith('/api/') &&
    !purpose.includes('prefetch')
  );
}
```

次のセクションで扱うルートミドルウェアは、ページ遷移時のロケールが記憶済みのロケールと異なる場合にクッキーを設定します。プリフェッチではクッキーが設定されないため、別の言語へのリンクがプリフェッチされても、買い物客が実際にそのリンクを開くまでカートの言語は切り替わりません。

## 言語を Shopify に渡す [#storefront-language]

ルートミドルウェアで各ロケールを Shopify の `LanguageCode` にマッピングし、`createShopifyRequestContext` に渡します。すると Storefront クライアントが、`@inContext(country: $country, language: $language)` を宣言しているすべてのクエリに `$language` と `$country` を自動で設定します。

```ts title="app/root.tsx"
import {
  createShopifyRequestContext,
  createStorefrontClient,
  handleShopifyRedirects,
  handleShopifyRoutes,
} from '@shopify/hydrogen';
import gtConfig from '../gt.config.json';
import {getRequestLocale, isPageNavigation, LOCALE_COOKIE_NAME, readCookie} from '~/lib/i18n';
import type {Route} from './+types/root';

// gt.config.json の各ロケールにつき 1 エントリ。
const SHOPIFY_LANGUAGES = {en: 'EN', fr: 'FR', ja: 'JA'} as const;

export const middleware: Route.MiddlewareFunction[] = [
  async ({context, request}, next) => {
    const locale = getRequestLocale(request) as keyof typeof SHOPIFY_LANGUAGES;
    const requestContext = createShopifyRequestContext({
      request,
      // 言語はページに合わせ、国は販売先のマーケットのまま変えません。
      // pathPrefix により、Hydrogen のバリアントおよび標準ルートのリダイレクト先がロケールの URL に維持されます。
      i18n: {
        language: SHOPIFY_LANGUAGES[locale],
        country: 'US',
        pathPrefix: locale === gtConfig.defaultLocale ? '' : `/${locale}`,
      },
    });
    const storefrontClient = createStorefrontClient({type: 'public', requestContext, config});

    const shopifyRoute = handleShopifyRoutes({request, requestContext, storefrontClient, routeTemplates, handlers});
    if (shopifyRoute) return shopifyRoute;

    context.set(storefrontClientContext, storefrontClient);

    const response = await next();
    if (response.status === 404) {
      const redirect = await handleShopifyRedirects({request, routeTemplates, storefrontClient});
      if (redirect) return redirect;
    }
    // Hydrogen の `/api` ハンドラー向けにページのロケールを記憶します。クッキーは
    // applyResponseHeaders より前に設定してください。applyResponseHeaders は、クッキーを設定するレスポンスをすべてプライベートかつキャッシュ不可にします。
    if (isPageNavigation(request) && readCookie(request, LOCALE_COOKIE_NAME) !== locale) {
      response.headers.append('Set-Cookie', `${LOCALE_COOKIE_NAME}=${locale}; Path=/; SameSite=Lax`);
    }
    storefrontClient.requestContext.applyResponseHeaders(response.headers);
    return response;
  },
];
```

`config`、`routeTemplates`、`handlers`、`storefrontClientContext` は既存の Hydrogen のセットアップにあるもので、新たに追加するのは `i18n` とロケールのクッキーだけです。クッキーを設定するページレスポンスはキャッシュされないため、この処理が行われるのは買い物客が最初にページを表示したときと、言語を切り替えたときだけです。

このガイドではすべての言語で米国マーケットを使用するため、フランス語に切り替えても表示テキストが変わるだけで、カートの購入者の国や通貨は変わりません。複数のマーケットを扱う場合は、買い物客が言語とは別に国を選択できるようにするか、各ロケールをマーケットに対応付け、言語の切り替え時にカートを移行してください ([複数のマーケットでの販売](/docs/integrations/shopify/guides/selling-in-markets)を参照) 。

## 価格の書式設定 [#prices]

Shopify の金額は、Shopify が返す通貨コードを使って書式設定します。[`<Currency>`](/docs/react/reference/components/currency) はアクティブなロケールに合わせて書式設定するだけで、金額の換算は行いません。

```tsx title="app/components/LocalizedMoney.tsx"
import {Currency} from 'gt-react';

/** 通貨を換算せずに、Shopify の価格をアクティブなロケールで書式設定します。 */
export function LocalizedMoney({data}: {data: {amount?: string; currencyCode?: string}}) {
  // 楽観的更新によるカートのレスポンスでは、cost フィールドが一時的に含まれない場合があります。
  if (data.amount == null || !data.currencyCode) return null;
  return <Currency currency={data.currencyCode}>{data.amount}</Currency>;
}
```

## ロケール内でリンクする [#links]

コード内のルートパスにはプレフィックスを付けず、リンクをレンダリングする際に現在のロケールのプレフィックスを追加します。[`useLocale`](/docs/react/reference/hooks/use-locale) でアクティブなロケールを取得し、`localizePath(to, locale)` を React Router の `Link` に渡してください。

言語を切り替えるには、現在の URL のプレフィックスを変更します。これによりルートローダーが新しいロケールを読み取り、カートの内容はそのまま維持されます。この場合は [`<LocaleSelector>`](/docs/react/reference/components/locale-selector) を使用しないでください。このコンポーネントはロケールをクッキーに保存して同じ URL を再読み込みしますが、その URL は引き続き古いロケールに解決されてしまいます。

```tsx title="app/components/LanguageSwitcher.tsx"
import {useLocale, useLocales} from 'gt-react';
import {useLocation, useNavigate} from 'react-router';
import {localizePath, stripLocale} from '~/lib/i18n';

/** URLのプレフィックスを変更して言語を切り替えます。ルートローダーがURLから新しいロケールを読み取ります。 */
export function LanguageSwitcher() {
  const locale = useLocale();
  const locales = useLocales();
  const {pathname, search} = useLocation();
  const navigate = useNavigate();
  return (
    <select
      aria-label="Language"
      value={locale}
      onChange={(event) => navigate(localizePath(stripLocale(pathname), event.target.value) + search)}
    >
      {locales.map((option) => (
        // 各言語名をその言語自身で表記します（例："français"、"日本語"）。
        <option key={option} value={option} lang={option}>
          {new Intl.DisplayNames([option], {type: 'language'}).of(option)}
        </option>
      ))}
    </select>
  );
}
```

`/account/login` などの Customer Account ルートへのリンクには、`Link` ではなく通常の `<a>` または `<form>` を使用してください。これらのルートのハンドラーは Shopify へリダイレクトしますが、client-side ナビゲーションではこのリダイレクトに追従できません。

## 翻訳を生成 [#generate]

テキストは [`<T>`](/docs/react/reference/components/t) でラップし、`aria-label` の値などの文字列には [`useGT`](/docs/react/reference/hooks/use-gt) を使用します。そのうえで、`react-router build` の前に [`npx gt translate`](/docs/cli/reference/commands/translate) を実行してください。ローダーによってバンドルされた翻訳は最初のサーバーレンダリングの時点で利用できるため、キャッシュされたレスポンスにも翻訳が含まれます。

ストアフロントの変更に合わせて、テキストのラップと翻訳を継続的に維持するには、リポジトリを [Locadex](/docs/platform/locadex/quickstart) に接続してください。Locadex が新しい翻訳を含むプルリクエストを作成するので、内容をレビューできます。

## Next steps

- /docs/integrations/shopify/guides/localizing-nextjs
- /docs/integrations/shopify/guides/translating-products
- /docs/integrations/shopify/guides/selling-in-markets

## Sitemap

See the full [sitemap](https://generaltranslation.com/sitemap.md) for all pages.
