このページとあなたの好きなAIアシスタントを使ってドキュメントを要約します
Intlayer MCPサーバーを統合することで、ChatGPT、DeepSeek、Cursor、VSCodeなどから直接ドキュメントを取得できます。
MCPサーバーのドキュメントを表示このページのコンテンツはAIを使用して翻訳されました。
英語の元のコンテンツの最新バージョンを見るこのドキュメントを改善するアイデアがある場合は、GitHubでプルリクエストを送信することで自由に貢献してください。
ドキュメントへのGitHubリンクドキュメントのMarkdownをクリップボードにコピー
Intlayer を使った next-intl による Next.js 15 の翻訳 | 国際化 (i18n)
このガイドでは、Next.js 15(App Router)アプリにおける next-intl のベストプラクティスを解説し、堅牢な翻訳管理と自動化のために Intlayer を重ねて使う方法を紹介します。
next-i18next vs next-intl vs Intlayer の比較はこちら をご覧ください。
- 初級者向け:ステップバイステップのセクションに従って、多言語対応アプリを作成しましょう。
- 中級者向け:ペイロードの最適化とサーバー/クライアントの分離に注意してください。
- 上級者向け:静的生成、ミドルウェア、SEO統合、自動化フックに注目してください。
本ガイドで扱う内容:
- セットアップとファイル構成
- メッセージの読み込み最適化
- クライアントおよびサーバーコンポーネントの使用
- メタデータ、サイトマップ、robots.txt によるSEO対策
- ロケールルーティングのためのミドルウェア
- Intlayer の追加(CLIと自動化)
next-intl を使ったアプリケーションのセットアップ
next-intl の依存関係をインストールします:
npm install next-intl.├── locales│ ├── en│ │ ├── common.json│ │ └── about.json│ ├── fr│ │ ├── common.json│ │ └── about.json│ └── es│ ├── common.json│ └── about.json└── src ├── i18n.ts ├── middleware.ts ├── app │ └── [locale] │ ├── layout.tsx │ └── about │ └── page.tsx └── components ├── ClientComponentExample.tsx └── ServerComponent.tsxセットアップとコンテンツの読み込み
ルートで必要な名前空間のみを読み込み、ロケールを早期に検証します。可能な限りサーバーコンポーネントは同期的に保ち、クライアントには必要なメッセージだけを渡します。
コードをクリップボードにコピー
import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";export const locales = ["en", "fr", "es"] as const;export const defaultLocale = "en" as const;async function loadMessages(locale: string) { // レイアウトやページで必要な名前空間のみを読み込みます const [common, about] = await Promise.all([ import(`../locales/${locale}/common.json`).then((m) => m.default), import(`../locales/${locale}/about.json`).then((m) => m.default), ]); return { common, about } as const;}export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale as any)) notFound(); return { messages: await loadMessages(locale), };});コードをクリップボードにコピー
import type { ReactNode } from "react";import { locales } from "@/i18n";import { getLocaleDirection, unstable_setRequestLocale,} from "next-intl/server";export const dynamic = "force-static";export function generateStaticParams() { return locales.map((locale) => ({ locale }));}export default async function LocaleLayout({ children, params,}: { children: ReactNode; params: { locale: string };}) { const { locale } = params; // このサーバーレンダリング(RSC)用にアクティブなリクエストのロケールを設定 unstable_setRequestLocale(locale); const dir = getLocaleDirection(locale); return ( <html lang={locale} dir={dir}> <body>{children}</body> </html> );}コードをクリップボードにコピー
import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponentExample from "@/components/ClientComponentExample";export const dynamic = "force-static";export default async function AboutPage({ params,}: { params: { locale: string };}) { const { locale } = params; // メッセージはサーバー側で読み込まれます。クライアントには必要なものだけを渡します。 const messages = await getMessages(); const clientMessages = pick(messages, ["common", "about"]); // 完全にサーバー側での翻訳/フォーマット処理 const tAbout = await getTranslations("about"); const tCounter = await getTranslations("about.counter"); const format = await getFormatter(); const initialFormattedCount = format.number(0); return ( <NextIntlClientProvider locale={locale} messages={clientMessages}> <main> <h1>{tAbout("title")}</h1> <ClientComponentExample /> <ServerComponent formattedCount={initialFormattedCount} label={tCounter("label")} increment={tCounter("increment")} /> </main> </NextIntlClientProvider> );}クライアントコンポーネントでの使用例
カウンターをレンダリングするクライアントコンポーネントの例を見てみましょう。
翻訳(形は再利用可能;お好みで next-intl のメッセージに読み込んでください)
コードをクリップボードにコピー
{ "counter": { "label": "Counter", "increment": "Increment" }}コードをクリップボードにコピー
{ "counter": { "label": "カウンター", "increment": "インクリメント" }}クライアントコンポーネント
コードをクリップボードにコピー
"use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => { // ネストされたオブジェクトに直接スコープを設定 const t = useTranslations("about.counter"); const format = useFormatter(); const [count, setCount] = useState(0); return ( <div> <p>{format.number(count)}</p> <button aria-label={t("label")} onClick={() => setCount((count) => count + 1)} > {t("increment")} </button> </div> );};ページのクライアントメッセージに "about" メッセージを追加するのを忘れないでください (クライアントが実際に必要とする名前空間のみを含めてください)。
サーバーコンポーネントでの使用方法
このUIコンポーネントはサーバーコンポーネントであり、クライアントコンポーネントの下でレンダリングできます(ページ → クライアント → サーバー)。事前に計算された文字列を渡して同期的に保ちます。
コードをクリップボードにコピー
type ServerComponentProps = { formattedCount: string; label: string; increment: string;};const ServerComponent = ({ formattedCount, label, increment,}: ServerComponentProps) => { return ( <div> <p>{formattedCount}</p> <button aria-label={label}>{increment}</button> </div> );};注意:
- formattedCount はサーバー側で計算してください(例:const initialFormattedCount = format.number(0))。
- サーバーコンポーネントに関数やシリアライズ不可能なオブジェクトを渡すことは避けてください。
コードをクリップボードにコピー
import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, url)]) ); return { title: t("title"), description: t("description"), alternates: { canonical: localizedPath(locale, url), languages: { ...languages, "x-default": url }, }, };}// ... ページの残りのコードコードをクリップボードにコピー
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export default function sitemap(): MetadataRoute.Sitemap { const aboutLanguages = Object.fromEntries( locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [ { url: formatterLocalizedPath(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", priority: 0.7, alternates: { languages: aboutLanguages }, }, ];}コードをクリップボードにコピー
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => "/" + locale + path),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...withAllLocales("/dashboard"), ...withAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: origin, sitemap: origin + "/sitemap.xml", };}ロケールルーティングのためのミドルウェア
ロケール検出とルーティングを処理するミドルウェアを追加します:
コードをクリップボードにコピー
import createMiddleware from "next-intl/middleware";import { locales, defaultLocale } from "@/i18n";export default createMiddleware({ locales: [...locales], defaultLocale, localeDetection: true,});export const config = { // API、Nextの内部処理、静的アセットをスキップ matcher: ["/((?!api|_next|.*\\..*).*)"],};ベストプラクティス
- htmlのlangとdirを設定する:src/app/[locale]/layout.tsxでgetLocaleDirection(locale)を使ってdirを計算し、<html lang={locale} dir={dir}>を設定します。
- メッセージを名前空間ごとに分割する:ロケールと名前空間ごとにJSONを整理します(例:common.json、about.json)。
- クライアントのペイロードを最小化する: ページでは、必要な名前空間のみを NextIntlClientProvider に送信します(例: pick(messages, ['common', 'about']))。
- 静的ページを優先する: export const dynamic = 'force-static' をエクスポートし、すべての locales に対して静的パラメータを生成します。
- 同期的なサーバーコンポーネント: 非同期呼び出しやシリアライズ不可能な関数の代わりに、事前に計算された文字列(翻訳済みラベル、フォーマット済み数値)を渡します。
next-intl の上に Intlayer を実装する
intlayer の依存関係をインストールします:
npm install intlayer @intlayer/sync-json-plugin -Dintlayer の設定ファイルを作成します:
コードをクリップボードにコピー
import { type IntlayerConfig, Locales } from "intlayer";import { syncJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = { internationalization: { locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH], defaultLocale: Locales.ENGLISH, }, ai: { apiKey: process.env.OPENAI_API_KEY, }, plugins: [ // 各ネームスペースのフォルダ構造をIntlayerと同期させる syncJSON({ source: ({ key, locale }) => `./locales/${locale}/${key}.json`, }), ],};export default config;package.jsonのスクリプトを追加:
コードをクリップボードにコピー
{ "scripts": { "i18n:fill": "intlayer fill", "i18n:test": "intlayer test" }}注意事項:
- intlayer fill: 設定されたロケールに基づいて、AIプロバイダーを使用して不足している翻訳を補完します。
- intlayer test: 不足している翻訳や無効な翻訳をチェックします(CIでの使用を推奨)。
引数やプロバイダーの設定方法については、Intlayer CLI を参照してください。