こんにちは!前回のエピソードでは、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.tsx
やsrc/app/[locale]/cart/page.tsx
もuseTranslations
を使って翻訳を適用します。例:
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_en
、title_ja
、description_en
、description_ja
などのメタフィールドを追加してください。
ステップ7: 動作確認
- 開発サーバーを起動(
npm run dev
)。 -
http://localhost:3000/en
とhttp://localhost:3000/ja
にアクセスし、以下の点を確認:- ページのテキスト(タイトル、ボタンなど)が正しい言語で表示される。
- 商品ページやカートページも翻訳されている。
- 言語を切り替えるとURLとコンテンツが適切に更新される。
- デベロッパーツールで
hreflang
タグが正しく設定されている。
- Shopifyのメタフィールドが正しく読み込まれ、商品の翻訳が表示されることを確認。
エラーがあれば、messages
ファイルやShopifyメタフィールドを確認してください。
まとめと次のステップ
このエピソードでは、next-intl
を使って多言語対応を実装し、グローバル市場向けにアプリを拡張しました。hreflang
タグや動的コンテンツの翻訳により、SEOとユーザー体験を向上させました。
次回のエピソードでは、Progressive Web App(PWA)機能を追加し、モバイルでのネイティブアプリのような体験を提供します。next-pwa
やプッシュ通知の実装も紹介しますので、引き続きお楽しみに!
この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!