Next.js 国際化と静的生成:SSG 多言語サイト構築の実践ガイド

Next.js App Router で初めて多言語対応の静的生成(SSG)に挑戦したとき、正直言ってかなり苦戦しました。
ドキュメント通りに設定したはずなのにビルドエラーが出たり、なんとか成功したと思ったら全ページの生成に15分もかかったり…。
あなたもこんな経験はありませんか?
こんな問題に直面していませんか?
シナリオ1:ビルド時の謎のエラー
意気揚々と npm run build を実行したら、ターミナルにこんなエラーが表示される。
Error: Page "/en/about" is missing `generateStaticParams()`
so it cannot be used with `output: "export"`.「え? next.config.js で i18n 設定したはずだけど?」と焦りますが、実は App Router と Pages Router では国際化の仕組みが全く異なるため、古い設定は通用しません。
シナリオ2:ビルド時間が長すぎる
私のプロジェクトでは6言語対応で、各言語50ページほどありました。合計300ページ。大規模とは言えませんが、ビルド結果は…
✓ Generating static pages (152/152) - 15m 32s15分!ちょっとした修正のたびにこれでは、開発体験は最悪です。「これで本番運用の CI/CD 回せるのか?」と不安になりました。
シナリオ3:翻訳が更新されない
最も厄介なのがこれです。ja.json を更新して再ビルド・デプロイしたのに、サイト上では古い翻訳のまま。ブラウザのキャッシュをクリアしないと反映されないなんて、本番環境では許されません。
問題の根本原因
これらの問題の背後には、いくつかの技術的な理由があります。
- Pages Router の i18n 設定は無効 - App Router は
next.config.jsのi18nフィールドを無視します。 - 静的エクスポートと動的レンダリングの競合 -
output: 'export'を使う場合、ビルド時に全ルートが確定している必要があります。cookies()やheaders()などの動的 API を使うとエラーになります。 - 翻訳ファイルのキャッシュ - Next.js はインポートされた JSON ファイルをキャッシュするため、開発中に更新が即座に反映されないことがあります。
この記事では、これらの落とし穴を避け、Next.js App Router で正しく、かつ効率的に多言語 SSG を実装する方法を解説します。
App Router における i18n の新パラダイム
コーディングを始める前に、App Router の国際化アプローチを理解しておきましょう。
Pages Router vs App Router:全く異なるアプローチ
| 機能 | Pages Router | App Router |
|---|---|---|
| 設定方法 | next.config.js の i18n | ミドルウェア + 動的ルート [lang] |
| URL構造 | /en/, /ja/ を自動付与 | app/[lang]/page.tsx を手動作成 |
| 静的生成 | getStaticPaths | generateStaticParams |
| 翻訳読込 | serverSideTranslations | Server Components で直接 import |
generateStaticParams とは?
App Router の核心となる関数です。Next.js に対して「このパラメータの組み合わせで静的ページを作ってくれ」と指示を出します。
// app/[lang]/layout.tsx
export async function generateStaticParams() {
// プリレンダリングしたい言語リストを返す
return [
{ lang: 'en' },
{ lang: 'ja' },
{ lang: 'zh' }
]
}ビルド時にこの関数が実行され、以下のような静的ファイルが出力されます:
out/
├── en/
│ └── index.html
├── ja/
│ └── index.html
└── zh/
└── index.html重要:関数名は厳密に generateStaticParams である必要があります。
翻訳ファイルの読み込み方
Pages Router 時代は next-i18next が主流でしたが、App Router ではシンプルに Server Components で JSON をインポートできます。
// Server Component 内
import enTranslations from '@/i18n/locales/en/common.json'
import jaTranslations from '@/i18n/locales/ja/common.json'
const translations = {
'en': enTranslations,
'ja': jaTranslations,
}
export default function Page({ params }: { params: { lang: string } }) {
const t = translations[params.lang]
return <h1>{t.title}</h1>
}ただし、これでは全言語の翻訳がバンドルに含まれてしまうため、実用的には動的インポートを用いるのが一般的です。
実践:ゼロから作る多言語 SSG プロジェクト
では、実際にコードを書いていきましょう。
手順1:ディレクトリ構造の設計
私が推奨する構造はこちらです:
app/
├── [lang]/ # 言語動的ルート(必須)
│ ├── layout.tsx # generateStaticParams を配置
│ ├── page.tsx # トップページ
│ ├── about/
│ │ └── page.tsx
│ └── blog/
│ ├── page.tsx
│ └── [slug]/
│ └── page.tsx # ネストされた動的ルート
├── i18n/
│ ├── locales/ # 翻訳ファイル
│ │ ├── en/...
│ │ ├── ja/...
│ │ └── zh-CN/...
│ ├── config.ts # 設定ファイル
│ └── utils.ts # ユーティリティ
└── middleware.ts # 言語リダイレクト用手順2:i18n 設定ファイル
// i18n/config.ts
export const i18nConfig = {
// サポート言語
locales: ['en', 'ja', 'zh-CN'],
// デフォルト言語
defaultLocale: 'en',
// 【重要】ビルド時間の最適化:主要言語のみプリレンダリングする
localesToPrerender: process.env.NODE_ENV === 'production'
? ['en', 'ja'] // 本番は英語と日本語のみ先に生成
: ['en'], // 開発時は英語のみ
} as const
export type Locale = (typeof i18nConfig)['locales'][number]
export const namespaces = ['common', 'home', 'blog'] as constポイント:localesToPrerender を活用することで、マイナー言語はアクセス時にオンデマンド生成(ISR)する戦略が取れます。
手順3:翻訳ローダーの実装
// i18n/utils.ts
import type { Locale, Namespace } from './config'
const translationsCache = new Map<string, any>()
export async function loadTranslations(
locale: Locale,
namespaces: Namespace[]
) {
const translations: Record<string, any> = {}
for (const namespace of namespaces) {
const cacheKey = `${locale}-${namespace}`
if (!translationsCache.has(cacheKey)) {
try {
const translation = await import(
`@/i18n/locales/${locale}/${namespace}.json`
)
translationsCache.set(cacheKey, translation.default)
} catch (error) {
console.warn(`⚠️ Translation file not found: ${locale}/${namespace}`)
translationsCache.set(cacheKey, {})
}
}
translations[namespace] = translationsCache.get(cacheKey)
}
return translations
}手順4:ルートレイアウト(最重要)
// app/[lang]/layout.tsx
import { i18nConfig } from '@/i18n/config'
import { loadTranslations } from '@/i18n/utils'
import type { Locale } from '@/i18n/config'
/**
* 【核心】静的パラメータの生成
* ビルド時に実行され、返す配列に基づいてページが生成されます
*/
export async function generateStaticParams() {
console.log(`🌍 Generating static params for ${i18nConfig.localesToPrerender.length} locales...`)
return i18nConfig.localesToPrerender.map((locale) => ({
lang: locale, // [lang] フォルダ名と一致させること
}))
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode
params: { lang: string }
}) {
const translations = await loadTranslations(params.lang as Locale, ['common'])
return (
<html lang={params.lang} dir={params.lang === 'ar' ? 'rtl' : 'ltr'}>
<body>{children}</body>
</html>
)
}注意点:generateStaticParams で返すオブジェクトのキー名は、フォルダ名 [lang] と一致させる必要があります(例:{ lang: 'en' })。{ locale: 'en' } だと動きません。
また、レイアウト内で cookies() などの動的 API を使わないように注意してください。使うと静的生成が失敗します。
手順5:ネストされた動的ルートの処理
ブログ記事などの詳細ページも静的生成する場合、言語 × 記事 の組み合わせを生成する必要があります。
// app/[lang]/blog/[slug]/page.tsx
// 記事スラッグ取得関数(ダミー)
async function getBlogSlugs() {
return ['getting-started', 'advanced-tips']
}
export async function generateStaticParams() {
// 全記事のスラッグを取得
const slugs = await getBlogSlugs()
// 言語 × 記事 の組み合わせを生成
// flatMap を使って配列をフラット化
return i18nConfig.localesToPrerender.flatMap((locale) =>
slugs.map((slug) => ({
lang: locale,
slug: slug,
}))
)
}
export default async function BlogPost({
params,
}: {
params: { lang: string; slug: string }
}) {
// コンポーネントロジック...
}パフォーマンス最適化のコツ:
ループの中で毎回データフェッチを行うのではなく、一度全データを取得してからメモリ上で組み合わせを作るのが定石です。これでビルド時間が大幅に短縮されます。
手順6:Middleware による言語検出
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { i18nConfig } from './i18n/config'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 既に言語プレフィックスがあるか確認
const hasLocale = i18nConfig.locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (hasLocale) return
// 言語検出ロジック(簡略版)
// 実際は Accept-Language ヘッダーや Cookie を解析します
const locale = i18nConfig.defaultLocale
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}パフォーマンス最適化:ビルド時間を爆速にする
サイト規模が大きくなるとビルド時間が問題になります。以下のテクニックで最適化しましょう。
1. 選択的プリレンダリング
全言語・全ページをビルド時に生成する必要はありません。アクセス頻度の高い主要言語(例:英語、日本語)だけ generateStaticParams に含め、他は含めないようにします。
含まれていないページへのリクエストが来たらどうなるか?export const dynamicParams = true(デフォルト)であれば、初回アクセス時にサーバーサイドで生成・キャッシュされます(ISRのような挙動)。
2. データ取得の並列化
ページコンポーネント内で複数のデータを取得する場合、Promise.all を使って並列化しましょう。
const [translations, post] = await Promise.all([
loadTranslations(params.lang, ['blog']),
getBlogPost(params.slug)
])まとめ
Next.js App Router での多言語 SSG は、正しく設定すれば非常に強力です。
- generateStaticParams で静的生成する言語とルートを明示する。
- 動的 API(Cookieなど)を静的ページで使わない。
- 選択的プリレンダリング でビルド時間をコントロールする。
これらのポイントを押さえれば、高速でSEOに強い多言語サイトが構築できます。
FAQ
generateStaticParams に含まれない言語はどうなりますか?
静的エクスポート(output: 'export')で動的機能は使えますか?
翻訳ファイルを更新しても反映されません
3 min read · 公開日: 2025年12月25日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます