i18n について
i18nは、next-intl v4を使ってNext.js App Routerプロジェクトを多言語化するスキルです。ロケールルーティング、翻訳ファイル、SEO向けの言語メタデータを設定する開発者向けです。
主な機能
- 01next-intl v4の設定
- 02ロケールルーティングとミドルウェアの設定
- 03UI文字列の抽出と翻訳ファイル化
- 04サイトマップへのhreflang追加
- 05翻訳とビルドの検証
活用シーン
- 01Next.js App Routerプロジェクトへの多言語ルーティング追加
- 02ページ文言の翻訳ファイルへの整理
- 03各ロケールのSEOメタデータ設定
- 04翻訳と静的ページのビルド確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ openclaudia/ openclaudia-skills --skill i18nインストール後、Claude Code で/i18nを入力して読み込まれたか確認します。
詳細
Next.js プロジェクトを国際化する
next-intl v4 を使用して、Next.js(App Router)プロジェクトに完全な国際化対応を追加します。このスキルでは、ルーティング、翻訳ファイル、サイトマップの hreflang、すべてのロケールを対象とした一括翻訳を扱います。
ステップ 1:プロジェクトを評価する
- Next.js のバージョン(
package.json)を確認します。App Router を使用した 13 以降のバージョンが必要です。 - i18n がすでに一部設定されているか確認します(
next-intl、next-i18next、[locale]ルートを調べます)。 - 翻訳が必要なすべてのページ/ルートを特定します。
- ユーザーに表示される文字列(コンポーネント内にハードコードされたテキスト)をすべて特定します。
- 対応するロケールをユーザーに確認します(デフォルトの推奨:en、es、fr、de、pt、ja、ar、zh、zh-tw、id、vi、ms、ru、hi)。
ステップ 2:依存関係をインストールする
npm install next-intl
ステップ 3:i18n 設定ファイルを作成する
src/i18n/ の下に 4 つのファイルを作成します。
src/i18n/config.ts
export const locales = ['en', 'es', 'fr', 'de', 'pt', 'ja', 'ar', 'zh', 'zh-tw', 'id', 'vi', 'ms', 'ru', 'hi'] as const
export type Locale = (typeof locales)[number]
export const defaultLocale: Locale = 'en'
export const localeNames: Record<Locale, string> = {
en: 'English',
es: 'Espanol',
fr: 'Francais',
de: 'Deutsch',
pt: 'Portugues',
ja: '日本語',
ar: 'العربية',
zh: '简体中文',
'zh-tw': '繁體中文',
id: 'Bahasa Indonesia',
vi: 'Tieng Viet',
ms: 'Bahasa Melayu',
ru: 'Русский',
hi: 'हिन्दी',
}
export const rtlLocales: Locale[] = ['ar']
src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing'
import { defaultLocale, locales } from './config'
export const routing = defineRouting({
locales,
defaultLocale,
localePrefix: 'as-needed', // English URLs stay clean, other locales get /es/, /fr/, etc.
})
src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)
src/i18n/request.ts
import { getRequestConfig } from 'next-intl/server'
import { routing } from './routing'
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
}
})
ステップ 4:ミドルウェアを作成する
src/middleware.ts を作成します。
import createMiddleware from 'next-intl/middleware'
import { routing } from '@/i18n/routing'
export default createMiddleware({
...routing,
localeDetection: false, // Don't auto-redirect based on Accept-Language
})
export const config = {
matcher: ['/((?!_next|api|images|fonts|favicon|sitemap|robots).*)'],
}
重要な判断:localeDetection: false にすると、ブラウザーの言語に基づく自動リダイレクトを防げます。これにより、SEO のために英語の URL を安定して維持できます。ユーザーは言語セレクターから手動で言語を切り替えられます。
ステップ 5:next.config を更新する
既存の設定を createNextIntlPlugin でラップします。
import createNextIntlPlugin from 'next-intl/plugin'
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
// ... existing config ...
export default withNextIntl(nextConfig)
ステップ 6:[locale] 動的ルートを追加する
すべてのページコンテンツを src/app/[locale]/ の下に移動します。
-
src/app/[locale]/layout.tsxを作成し、次の内容を含めます。- すべてのロケールを返す
generateStaticParams() setRequestLocale(locale)の呼び出し- 子要素をラップする
<NextIntlClientProvider> <html lang={locale} dir={rtlLocales.includes(locale) ? 'rtl' : 'ltr'}>- すべてのロケールと
x-defaultを含む<head>内の hreflang<link>タグ
- すべてのロケールを返す
-
既存のページを
src/app/[locale]/に移動します。 -
各ページで静的生成のために
setRequestLocale(locale)を呼び出します。
ステップ 7:文字列を翻訳ファイルに抽出する
-
ユーザーに表示されるすべての文字列をセクションごとに整理した
src/messages/en.jsonを作成します。{ "common": { "signIn": "Sign In", ... }, "tools": { "tool-slug": { "title": "...", "description": "..." } }, "faq": { "tool-slug": [{ "question": "...", "answer": "..." }] } } -
コンポーネント内のハードコードされた文字列をすべて
useTranslations()に置き換えます。const t = useTranslations('common') return <button>{t('signIn')}</button> -
サーバーコンポーネントでは
getTranslations()を使用します。const t = await getTranslations('common')
ステップ 8:すべてのロケールに翻訳する
英語以外の各ロケールについて、en.json と同じ構造の src/messages/{locale}.json を作成します。
翻訳戦略
Claude のクレジットを節約するため、codex-tasks スキルを使って 並列 Codex エージェント を使用します。
/codex-tasksを使い、ロケールごとに Codex タスクを 1 つ起動します(並列実行は最大 7 つ)。- 各タスクで
en.jsonを読み込み、すべての文字列を翻訳して{locale}.jsonに書き込みます。 - Codex へのプロンプトには次の内容を含めます。
en.jsonの全文(または読み込むファイルパス)- 翻訳先の言語名とロケールコード
- 指示:
- 直訳ではなく、自然に翻訳する
- 技術用語は英語のままにする(PowerPoint、PDF、API など)
- JSON の構造を完全に維持する(キーとネストを同じにする)
{count}、{name}などの補間変数を変更しない- 結果を
src/messages/{locale}.jsonに書き込む
- Codex タスクの完了後、以下の検証スクリプトを使って結果を確認します。Codex の出力品質にはばらつきがあるため、確認が必要です。
検証
翻訳後、問題を検出する検証スクリプトを実行します。
import json
locales = ['es', 'fr', 'de', 'pt', 'ja', 'ar', 'zh', 'zh-tw', 'id', 'vi', 'ms', 'ru', 'hi']
english_words = ['the ', 'and ', 'you ', 'your ', 'our ', 'this ', 'that ', 'with ', 'from ', 'will ']
with open('src/messages/en.json') as f:
en = json.load(f)
for loc in locales:
with open(f'src/messages/{loc}.json') as f:
data = json.load(f)
# Check: missing sections
missing = [s for s in en if s not in data]
# Check: residual English content
eng_count = 0
def check(d):
nonlocal eng_count # won't work in inline script; use list trick
if isinstance(d, dict):
for v in d.values(): check(v)
elif isinstance(d, str):
if sum(1 for w in english_words if w in d.lower()) >= 3:
eng_count += 1
check(data)
status = 'OK' if not missing and eng_count == 0 else 'ISSUES'
print(f'{loc}: {status} (missing={len(missing)}, english={eng_count})')
ステップ 9:hreflang を使ってサイトマップを更新する
src/app/sitemap.ts を更新して、hreflang の代替 URL を追加します。
import { MetadataRoute } from 'next'
import { locales } from '@/i18n/config'
const baseUrl = 'https://www.example.com'
function buildAlternates(path: string): Record<string, string> {
const alternates: Record<string, string> = {}
for (const locale of locales) {
const prefix = locale === 'en' ? '' : `/${locale}`
alternates[locale] = `${baseUrl}${prefix}${path}`
}
return alternates
}
export default function sitemap(): MetadataRoute.Sitemap {
return pages.map((path) => ({
url: `${baseUrl}${path}`,
lastModified: new Date(),
alternates: { languages: buildAlternates(path) },
}))
}
重要:ロケールごとに 1 つずつ URL を作成するのではなく、hreflang の代替 URL を設定したページごとの正規 URL を 1 つ生成します。これにより、検索結果での重複コンテンツを防げます。
ステップ 10:言語セレクターを追加する(任意)
@/i18n/navigation の useRouter と usePathname を使用し、現在のパスを維持したままロケールを切り替える言語切り替えコンポーネントを追加します。
ステップ 11:検証
- プロジェクトをビルドします:
npm run build— すべての静的ページが正しく生成されることを確認します - 英語の URL にプレフィックスがないことを確認します:
https://example.com/tools - ロケールの URL にプレフィックスがあることを確認します:
https://example.com/es/tools - サイトマップに hreflang の代替 URL があることを確認します
- アラビア語の RTL 表示を確認します
- ステップ 8 の翻訳検証スクリプトを実行します
よくある落とし穴
public/sitemap.xmlは、開発モードで動的なsrc/app/sitemap.tsと競合します — 静的ファイルを削除するか、名前を変更してください- ミドルウェアの matcher では、
_next、api、sitemap、robots、静的アセットのパスを除外する必要があります localePrefix: 'as-needed'は非常に重要です — SEO の継続性を保つため、デフォルトのロケール URL を簡潔にしますlocaleDetection: falseは、SEO を損ない、ユーザーを混乱させる不要なリダイレクトを防ぎます- 大きな翻訳ファイル(ロケールごとに 5000 行以上)は、git のプッシュが失敗する原因になることがあります —
git config http.postBuffer 524288000を使用してください - 翻訳を十分に検証してください — 自動翻訳では言語が混在した出力になることがよくあります。Codex のタスク完了後は、必ず英単語検出スクリプトで検証してください
ロケール数の参考情報
- 14 ロケール x N ページ = ビルド時に 14N 個の静的ページ
- 各ロケールの JSON ファイルは通常、en.json の 2~5 倍のサイズです(CJK 文字、冗長な言語)
- ロケール数に比例してビルド時間が増加します