2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

こんにちは!前回のエピソードでは、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での国際化を簡単にするライブラリです。まず、必要なパッケージをインストールします:

npm install next-intl

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

/** @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(英語):
{
  "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(日本語):
{
  "Home": {
    "title": "商品一覧",
    "searchPlaceholder": "商品を検索..."
  },
  "ProductPage": {
    "addToCart": "カートに追加",
    "variants": "バリエーション"
  },
  "Cart": {
    "title": "ショッピングカート",
    "empty": "カートに商品がありません",
    "total": "合計"
  },
  "Checkout": {
    "title": "チェックアウト",
    "email": "メールアドレス",
    "address": "配送先住所",
    "submit": "決済に進む"
  }
}

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

next-intlを使って多言語ルーティングを構築します。src/app/[locale]/layout.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の更新例:

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.tsxsrc/app/[locale]/cart/page.tsxuseTranslationsを使って翻訳を適用します。例:

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>を追加:

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を更新:

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_entitle_jadescription_endescription_jaなどのメタフィールドを追加してください。


ステップ7: 動作確認

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

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


まとめと次のステップ

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

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


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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?