Next.jsで高速eコマースアプリを構築する | エピソード8: 多言語対応と国際化（i18n）

こんにちは！前回のエピソードでは、Algoliaを使って検索とフィルタリング機能を追加し、ユーザー体験を向上させました。今回は、グローバル市場向けにアプリを拡張するため、多言語対応（i18n）を実装します。`next-intl`を使って多言語ルーティングを構築し、SEOに最適化された多言語ページを作成します。商品やカテゴリの動的コンテンツも言語に応じて表示しましょう！

## このエピソードのゴール

- `next-intl`を使って多言語対応を実装。
- 多言語ルーティング（例: `/en`, `/ja`）をNext.jsで設定。
- SEOに最適化された多言語ページ（hreflangタグ）を構築。
- 商品やカテゴリの動的コンテンツを言語に応じて表示。

## 必要なもの

- 前回のプロジェクト（`next-ecommerce`）がセットアップ済み。
- `next-intl`パッケージ。
- 基本的なTypeScript、React、Next.jsの知識。

---

## ステップ1: next-intlのセットアップ

`next-intl`は、Next.jsでの国際化を簡単にするライブラリです。まず、必要なパッケージをインストールします：

```bash
npm install next-intl
```

`next.config.js`を更新して`next-intl`を統合：

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['cdn.shopify.com'],
  },
};

const withNextIntl = require('next-intl/plugin')();

module.exports = withNextIntl(nextConfig);
```

---

## ステップ2: 多言語設定の準備

サポートする言語（例: 英語と日本語）の翻訳ファイルを準備します。`messages`ディレクトリを作成し、翻訳ファイルを追加：

- `messages/en.json`（英語）：

```json
{
  "Home": {
    "title": "Product List",
    "searchPlaceholder": "Search products..."
  },
  "ProductPage": {
    "addToCart": "Add to Cart",
    "variants": "Variants"
  },
  "Cart": {
    "title": "Shopping Cart",
    "empty": "Your cart is empty",
    "total": "Total"
  },
  "Checkout": {
    "title": "Checkout",
    "email": "Email Address",
    "address": "Shipping Address",
    "submit": "Proceed to Payment"
  }
}
```

- `messages/ja.json`（日本語）：

```json
{
  "Home": {
    "title": "商品一覧",
    "searchPlaceholder": "商品を検索..."
  },
  "ProductPage": {
    "addToCart": "カートに追加",
    "variants": "バリエーション"
  },
  "Cart": {
    "title": "ショッピングカート",
    "empty": "カートに商品がありません",
    "total": "合計"
  },
  "Checkout": {
    "title": "チェックアウト",
    "email": "メールアドレス",
    "address": "配送先住所",
    "submit": "決済に進む"
  }
}
```

---

## ステップ3: 多言語ルーティングの設定

`next-intl`を使って多言語ルーティングを構築します。`src/app/[locale]/layout.tsx`ファイルを作成：

```tsx
import { NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
import Navbar from '@/components/Navbar';
import '../styles/globals.css';

export function generateStaticParams() {
  return [{ locale: 'en' }, { locale: 'ja' }];
}

export default async function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  let messages;
  try {
    messages = (await import(`../../messages/${locale}.json`)).default;
  } catch (error) {
    notFound();
  }

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider locale={locale} messages={messages}>
          <Navbar />
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}
```

このコードは：
- サポートする言語（`en`, `ja`）を静的パラメータとして定義。
- 各言語の翻訳ファイルを動的に読み込み。
- `NextIntlClientProvider`で翻訳データをクライアントに提供。

---

## ステップ4: ページの多言語対応

主要なページを更新して翻訳を利用します。以下は、`src/app/[locale]/page.tsx`の更新例：

```tsx
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import Image from 'next/image';
import { getProducts } from '@/lib/shopify';

interface Product {
  id: string;
  title: string;
  handle: string;
  priceRange: {
    minVariantPrice: {
      amount: string;
      currencyCode: string;
    };
  };
  images: {
    edges: Array<{
      node: {
        url: string;
        altText: string | null;
      };
    }>;
  };
}

export default async function Home({ params: { locale } }: { params: { locale: string } }) {
  const products = await getProducts(10);
  const t = useTranslations('Home');

  return (
    <main className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">{t('title')}</h1>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
        {products.map((product) => (
          <Link href={`/${locale}/products/${product.handle}`} key={product.id}>
            <div className="border rounded-lg p-4 hover:shadow-lg transition">
              <Image
                src={product.images.edges[0]?.node.url}
                alt={product.images.edges[0]?.node.altText || product.title}
                width={200}
                height={200}
                className="w-full h-48 object-cover rounded"
                loading="lazy"
                style={{ aspectRatio: '1 / 1' }}
              />
              <h2 className="text-xl font-semibold mt-2">{product.title}</h2>
              <p className="text-gray-600">
                {parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2)}{' '}
                {product.priceRange.minVariantPrice.currencyCode}
              </p>
            </div>
          </Link>
        ))}
      </div>
    </main>
  );
}
```

同様に、`src/app/[locale]/products/[handle]/page.tsx`や`src/app/[locale]/cart/page.tsx`も`useTranslations`を使って翻訳を適用します。例：

```tsx
import { useTranslations } from 'next-intl';

// ... 既存のコード ...

export default function CartPage() {
  const t = useTranslations('Cart');
  const { items, updateQuantity, removeItem, clearCart } = useCartStore();

  return (
    <main className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">{t('title')}</h1>
      {/* 既存のJSX、翻訳キーを使用 */}
    </main>
  );
}
```

---

## ステップ5: SEOのためのhreflangタグ

多言語ページのSEOを最適化するため、`hreflang`タグを追加します。`src/app/[locale]/layout.tsx`に`<Head>`を追加：

```tsx
import { NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
import Navbar from '@/components/Navbar';
import Head from 'next/head';
import '../styles/globals.css';

export function generateStaticParams() {
  return [{ locale: 'en' }, { locale: 'ja' }];
}

export default async function LocaleLayout({
  children,
  params: { locale },
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {
  let messages;
  try {
    messages = (await import(`../../messages/${locale}.json`)).default;
  } catch (error) {
    notFound();
  }

  const alternateLocales = ['en', 'ja'].filter((l) => l !== locale);

  return (
    <html lang={locale}>
      <Head>
        <link rel="alternate" hrefLang={locale} href={`https://yourdomain.com/${locale}`} />
        {alternateLocales.map((altLocale) => (
          <link
            key={altLocale}
            rel="alternate"
            hrefLang={altLocale}
            href={`https://yourdomain.com/${altLocale}`}
          />
        ))}
        <link rel="alternate" hrefLang="x-default" href="https://yourdomain.com/en" />
      </Head>
      <body>
        <NextIntlClientProvider locale={locale} messages={messages}>
          <Navbar />
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}
```

このコードは：
- 各言語のページに`hreflang`タグを追加。
- デフォルト言語（例: `en`）を`x-default`として指定。
- 検索エンジンが正しい言語バージョンを表示するよう支援。

**注意**: `yourdomain.com`は実際のドメインに置き換えてください。

---

## ステップ6: 動的コンテンツの翻訳

商品のタイトルや説明はShopifyから取得されるため、動的に翻訳する必要があります。Shopifyのメタフィールドを使って多言語データを保存し、取得します。例として、`src/lib/shopify.ts`を更新：

```ts
export async function getProductByHandle(handle: string, locale: string): Promise<ProductDetail | null> {
  const data = await shopifyClient.request<ProductDetailResponse>(GET_PRODUCT_BY_HANDLE_QUERY, {
    handle,
  });
  const product = data.productByHandle;
  if (!product) return null;

  // メタフィールドから翻訳を取得（例）
  const translatedTitle = product.metafields?.find((mf) => mf.key === `title_${locale}`)?.value || product.title;
  const translatedDescription = product.metafields?.find((mf) => mf.key === `description_${locale}`)?.value || product.descriptionHtml;

  return {
    ...product,
    title: translatedTitle,
    descriptionHtml: translatedDescription,
  };
}
```

Shopify管理画面で、各商品に`title_en`、`title_ja`、`description_en`、`description_ja`などのメタフィールドを追加してください。

---

## ステップ7: 動作確認

1. 開発サーバーを起動（`npm run dev`）。
2. `http://localhost:3000/en`と`http://localhost:3000/ja`にアクセスし、以下の点を確認：
   - ページのテキスト（タイトル、ボタンなど）が正しい言語で表示される。
   - 商品ページやカートページも翻訳されている。
   - 言語を切り替えるとURLとコンテンツが適切に更新される。
   - デベロッパーツールで`hreflang`タグが正しく設定されている。
3. Shopifyのメタフィールドが正しく読み込まれ、商品の翻訳が表示されることを確認。

エラーがあれば、`messages`ファイルやShopifyメタフィールドを確認してください。

---

## まとめと次のステップ

このエピソードでは、`next-intl`を使って多言語対応を実装し、グローバル市場向けにアプリを拡張しました。`hreflang`タグや動的コンテンツの翻訳により、SEOとユーザー体験を向上させました。

次回のエピソードでは、Progressive Web App（PWA）機能を追加し、モバイルでのネイティブアプリのような体験を提供します。`next-pwa`やプッシュ通知の実装も紹介しますので、引き続きお楽しみに！

---

この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです！次回のエピソードもお楽しみに！