Next.js(App Router) 多言語対応の手順
Next.js App Router のInternationalizationを使ってNext.jsの多言語化対応の手順についての備忘録です。
参考一覧
バージョン情報
Next.js 14.2.4
DEMO
Github / デモサイト
ソースコード
ミドルウェア
ミドルウェアの役割について
ミドルウェアを使えば、リクエストが完了する前にコードを実行することができる。そして、送られてきたリクエストに基づいて、レスポンスを書き換えたり、リダイレクトしたり、リクエストやレスポンスのヘッダーを変更したり、直接レスポンスしたりすることで、レスポンスを変更することができる。
ミドルウェアはキャッシュされたコンテンツとルートがマッチする前に実行されます。詳細はパスのマッチングを参照してください。
https://nextjs.org/docs/app/building-your-application/routing/middleware
多言語設定を行うミドルウェアでは、ページのGETリクエストに言語指定が含まれているかを確認して、最適な言語へリダイレクトする機能をページレンダリングを実行する前に行います。
middleware
関数
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = i18n.locales.every(
(locale) =>
!pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
);
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
request.url,
),
);
}
}
- リクエストのパスにロケールが含まれていないかをチェックします。
- ロケールが含まれていない場合、
getLocale
関数を使用して最適なロケールを取得し、そのロケールを含む新しいURLにリダイレクトします。
getLacale
関数
function getLocale(request: NextRequest): string | undefined {
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
const locales: string[] = i18n.locales;
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(locales);
const locale = matchLocale(languages, locales, i18n.defaultLocale);
return locale;
}
- Negotiator を使用してリクエストヘッダーから言語情報を取得し、matchLocale を使用して最適なロケールを選択します。
config
オブジェクト
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
- ミドルウェアから除外するパスを指定
middleware.ts 全文
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { i18n } from "./i18n-config";
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
function getLocale(request: NextRequest): string | undefined {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders: Record<string, string> = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
// @ts-ignore locales are readonly
const locales: string[] = i18n.locales;
// Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(
locales,
);
const locale = matchLocale(languages, locales, i18n.defaultLocale);
return locale;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
// // If you have one
// if (
// [
// '/manifest.json',
// '/favicon.ico',
// // Your other files in `public`
// ].includes(pathname)
// )
// return
// Check if there is any supported locale in the pathname
const pathnameIsMissingLocale = i18n.locales.every(
(locale) =>
!pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
);
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`,
request.url,
),
);
}
}
export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
ロケールスイッチ(言語切替スイッチ)
このコンポーネントは、ユーザーがボタンをクリックすることで、ページのロケールを "ja"(日本語)と "en"(英語)の間で切り替えることができる機能を提供します。ロケールが切り替わると、URL が更新され、対応する言語のページにリダイレクトされます。
LocaleSwitcher
コンポーネント
export default function LocaleSwitcher() {
const pathName = usePathname();
const router = useRouter();
const [currentLocale, setCurrentLocale] = useState<Locale>("ja");
useEffect(() => {
const segments = pathName?.split("/");
if (segments && i18n.locales.includes(segments[1] as Locale)) {
setCurrentLocale(segments[1] as Locale);
}
}, [pathName]);
- pathName には現在のパスが格納されます。
- router はルーターオブジェクトで、ページ遷移に使用します。
- currentLocale は現在のロケールを管理するための状態です。
- useEffect フックは、パスが変更されたときに実行され、パスのセグメントからロケールを取得して currentLocale を更新します。
toggleLocale
関数
const toggleLocale = () => {
const newLocale: Locale = currentLocale === "en" ? "ja" : "en";
const redirectedPathName = (locale: Locale) => {
if (!pathName) return "/";
const segments = pathName.split("/");
segments[1] = locale;
return segments.join("/");
};
router.push(redirectedPathName(newLocale));
setCurrentLocale(newLocale);
};
- toggleLocale 関数は、現在のロケールを切り替え、新しいロケールに基づいてパスを再構築し、ページをリダイレクトします。
- newLocale は現在のロケールが "en" なら "ja" に、"ja" なら "en" に切り替えます。
- redirectedPathName 関数は、新しいロケールを含むパスを生成します。
- router.push を使用して新しいパスにリダイレクトし、currentLocale を更新します。
Locale-Switcher 全文
"use client";
import { usePathname, useRouter } from "next/navigation";
import { i18n, type Locale } from "@/i18n-config";
import { useState, useEffect } from "react";
import styles from "./index.module.css";
export default function LocaleSwitcher() {
const pathName = usePathname();
const router = useRouter();
const [currentLocale, setCurrentLocale] = useState<Locale>("ja");
useEffect(() => {
const segments = pathName?.split("/");
if (segments && i18n.locales.includes(segments[1] as Locale)) {
setCurrentLocale(segments[1] as Locale);
}
}, [pathName]);
const toggleLocale = () => {
const newLocale: Locale = currentLocale === "en" ? "ja" : "en";
const redirectedPathName = (locale: Locale) => {
if (!pathName) return "/";
const segments = pathName.split("/");
segments[1] = locale;
return segments.join("/");
};
router.push(redirectedPathName(newLocale));
setCurrentLocale(newLocale);
};
return (
<div>
<button className={`${styles.switcher} ${styles.active}`} onClick={toggleLocale}>
{currentLocale === "ja" ? "English" : "日本語"}
</button>
</div>
);
}
.switcher {
display: inline-block;
padding: 10px 20px;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
color: #333;
transition: background-color 0.3s, color 0.3s;
}
.switcher:hover {
background-color: #333;
color: #fff;
}
.active {
background-color: #fff0;
color: #333;
border-color: #333;
}
コンフィグファイルについて
コンフィグファイルは、ルートディレクトリに配置します。
export const i18n = {
defaultLocale: "ja",
locales: ["en", "ja"],
} as const;
export type Locale = (typeof i18n)["locales"][number];
ロケールの型情報と、デフォルトのロケールを設定します。
import "server-only";
import type { Locale } from "./i18n-config";
// We enumerate all dictionaries here for better linting and typescript support
// We also get the default import for cleaner types
const dictionaries = {
en: () => import("./dictionaries/en.json").then((module) => module.default),
ja: () => import("./dictionaries/ja.json").then((module) => module.default)
};
export const getDictionary = async (locale: Locale) => dictionaries[locale]?.() ?? dictionaries.ja();
このコードは、サーバーサイドで使用される辞書データを動的にインポートし、指定されたロケールに対応する辞書データを取得するための関数を提供します。
これにより、異なる言語の辞書データを効率的に管理し、必要に応じて動的にロードすることができます。
// /dictionaries/en.json
{
"server-component": {
"a": "Welcome to the new server component interface. This area allows you to manage and monitor all your server activities.",
"b": "The quick brown fox jumps over the lazy dog. This sentence contains every letter in the English alphabet.",
"c": "Ensuring that every user has a seamless and efficient experience is our top priority. We continuously strive for excellence.",
"d": "This is a test sentence designed to verify language display. It is important for multilingual applications to be accurate.",
"e": "The advancement of technology has significantly impacted our daily lives, providing convenience and efficiency in various tasks.",
"f": "Learning a new language can be challenging but also incredibly rewarding. It opens up new opportunities and perspectives.",
"g": "Effective communication is key to success in any field. Clear and concise messaging ensures that your audience understands your point.",
"h": "In today's globalized world, being multilingual is an invaluable skill. It enhances personal and professional growth.",
"i": "Reading books is a great way to gain knowledge and relax. It stimulates the mind and enhances creativity.",
"j": "Staying updated with current events helps us make informed decisions. It is essential to be aware of what is happening around us."
}
}
// /dictionaries/ja.json
{
"server-component": {
"a": "新しいサーバーコンポーネントインターフェースへようこそ。このエリアでは、すべてのサーバーアクティビティを管理および監視できます。",
"b": "素早い茶色の狐が怠けた犬の上を飛び越える。この文章は英語アルファベットのすべての文字を含んでいます。",
"c": "すべてのユーザーにシームレスで効率的な体験を提供することが私たちの最優先事項です。私たちは常に卓越性を追求しています。",
"d": "これは言語表示を検証するためのテスト文です。多言語アプリケーションが正確であることが重要です。",
"e": "技術の進歩は私たちの日常生活に大きな影響を与え、さまざまなタスクにおいて利便性と効率を提供しています。",
"f": "新しい言語を学ぶことは挑戦的ですが、非常にやりがいがあります。それは新しい機会と視点を開きます。",
"g": "効果的なコミュニケーションはどの分野においても成功の鍵です。明確で簡潔なメッセージングは、聴衆があなたのポイントを理解するのに役立ちます。",
"h": "今日のグローバル化された世界では、多言語が非常に価値のあるスキルです。それは個人的および職業的な成長を促進します。",
"i": "読書は知識を得てリラックスするのに最適な方法です。それは心を刺激し、創造性を高めます。",
"j": "現在の出来事を把握することは、私たちが情報に基づいた意思決定を行うのに役立ちます。周囲で何が起こっているのかを知ることが重要です。"
}
}
Discussion