🌏
Next.js App Router + 静的エクスポートで実現する多言語対応アーキテクチャ
背景と要件
以下の制約条件下で多言語対応を実現したい。
- Next.js 15 App Router with TypeScript
-
Static Export (
output: 'export') - S3 + CloudFront での配信 - サーバーサイドレンダリング不可 - 動的APIやミドルウェアが使用できない
- SEO対応必須 - 検索エンジンに適切な言語コンテンツを提供
アーキテクチャ概要
1. ルーティング設計
/ # ルートページ (デフォルト: en)
/en/ # 英語ページ
/ja/ # 日本語ページ
Dynamic Segment [locale] を使用し、静的生成時に各言語のページを事前レンダリングします。
2. 翻訳データ管理
翻訳データはJSON形式で管理:
src/locales/en.json
{
"title": "title"
}
src/locales/ja.json
{
"title": "タイトル"
}
3. React Context API による状態管理
src/i18n/context.tsx
interface I18nContextType {
messages: Record<string, any>;
locale: string;
}
const I18nContext = createContext<I18nContextType | undefined>(undefined);
export function I18nProvider({
children,
messages,
locale
}: I18nProviderProps) {
return (
<I18nContext.Provider value={{ messages, locale }}>
{children}
</I18nContext.Provider>
);
}
export function useTranslations() {
const context = useContext(I18nContext);
if (!context) {
throw new Error('useTranslations must be used within I18nProvider');
}
const t = (key: string): string => {
// ネストしたキーをサポート
const keys = key.split('.');
let value: any = context.messages;
for (const k of keys) {
value = value?.[k];
}
return typeof value === 'string' ? value : key;
};
return {
t,
locale: context.locale,
messages: context.messages,
isLoaded: true
};
}
4. レイアウト階層での Context 提供
src/app/layout.tsx
export default async function RootLayout({ children }: Props) {
const locale = defaultLocale;
const messages = await getMessages(locale);
return (
<html lang={locale}>
<body>
<I18nProvider messages={messages} locale={locale}>
<LanguageSwitcherClient />
{children}
</I18nProvider>
</body>
</html>
);
}
src/app/[locale]/layout.tsx
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
const messages = await getMessages(locale);
return (
<div lang={locale}>
<I18nProvider messages={messages} locale={locale}>
{children}
</I18nProvider>
</div>
);
}
デフォルト言語とクライアントサイドリダイレクト
サーバーサイド (静的生成時)
ルートページ (/) は常に 英語 (en) で生成されます:
src/app/page.tsx
export default async function RootPage() {
const locale = defaultLocale; // 'en'
const messages = await getMessages(locale);
return (
<I18nProvider messages={messages} locale={locale}>
<ClientLocaleRedirect />
<PageContent />
</I18nProvider>
);
}
理由:
- SEO対策: 検索エンジンクローラーに安定したコンテンツを提供
- 静的エクスポート制約: サーバー側でユーザー言語を判定できない
クライアントサイド (ブラウザ)
初回アクセス時、ブラウザの言語設定を検出して自動リダイレクト:
src/components/ClientLocaleRedirect.tsx
'use client';
export default function ClientLocaleRedirect() {
useEffect(() => {
// リダイレクト済みかチェック
if (typeof window === 'undefined' ||
sessionStorage.getItem('localeRedirectDone')) {
return;
}
const supportedLocales = ['en', 'ja'];
const pathname = window.location.pathname;
// ルートパスの場合のみリダイレクト
if (pathname === '/') {
// ブラウザの言語設定を取得
const browserLocales = navigator.languages || [navigator.language];
// サポートされている言語を探す
const preferredLocale = browserLocales
.map(lang => lang.split('-')[0]) // 'ja-JP' -> 'ja'
.find(lang => supportedLocales.includes(lang));
// デフォルトと異なる場合はリダイレクト
if (preferredLocale && preferredLocale !== defaultLocale) {
sessionStorage.setItem('localeRedirectDone', 'true');
window.location.replace(`/${preferredLocale}`);
}
}
}, []);
return null;
}
動作シナリオ
| 状況 | SSG時 | クライアント動作 |
|---|---|---|
| 検索エンジンクローラー |
/ を英語で生成 |
リダイレクトなし |
| 日本語ブラウザユーザー |
/ を英語で生成 |
/ja へリダイレクト |
| 英語ブラウザユーザー |
/ を英語で生成 |
リダイレクトなし (そのまま表示) |
| 未サポート言語ユーザー |
/ を英語で生成 |
リダイレクトなし (英語表示) |
コンポーネントでの使用方法
'use client';
function NetworkInfoClient() {
const { t } = useTranslations();
return (
<div>
<h2>{t('title')}</h2>
</div>
);
}
技術的な利点
- 型安全性: TypeScript + Context API により、翻訳キーの存在チェックが可能
-
依存関係の明確化:
useTranslationsを使うコンポーネントは必ず Provider 内に配置する必要があり、設計が明確 - パフォーマンス: 静的生成により高速なページ配信
- SEO: 各言語ページが独立したURLを持ち、検索エンジンに最適化
- UX: ブラウザ言語設定による自動リダイレクトで、ユーザーの手間を削減
まとめ
Next.js App Router の静的エクスポート環境下で、React Context API を活用した多言語対応を実現しました。サーバーサイドの制約をクライアントサイドのロジックで補完し、SEOとUXの両立を達成しています。
デフォルト言語の変更は config.ts の defaultLocale を編集するだけで対応可能で、拡張性も確保されています。
[PR]つくったアプリはこちら📱
Discussion