Next.js 13 の App Routerを使用したアプリケーションを多言語にする方法は、i18n with Next.js 13 and app directory やこちらで紹介されています。この記事では何時でも使えるよう、備忘録として残しておきたいと思います。
App Router
TypeScript版
アプリ作成とパッケージをインストール
アプリを作成します。
npx create-next-app@latest
App Router(appディレクトリ)を使用した多言語対応に必要なパッケージをインストールします。
npm install i18next react-i18next i18next-resources-to-backend accept-language
i18next | JavaScript 環境向けのアプリを多言語対応させます。 |
react-i18next | React コンポーネントで i18next を使用する際に使用します。 |
i18next-resources-to-backend | リソースを i18next バックエンドに変換するのに役立ちます。 |
accept-language | HTTP Accept-Language ヘッダーを解析し、一致する定義済み言語を返します。 |
ファイル構成
|-- middleware.js
|-- app//
| `-- [lng]//
| |-- layout.js
| |-- page.js
| `-- client-page//
| `-- page.js
|-- components//
| `-- LngButton//
| |-- index.js
| `-- client.js
`-- i18n//
|-- languages//
| |-- en//
| | `-- main.json
| `-- ja//
| `-- main.json
|-- client.js
|-- index.js
`-- settings.js
コードと備忘録
i18n
サーバーコンポーネント、クライアントコンポーネント用を作成します。
共通の設定ファイル
export const fallbackLng = "ja";
export const languages = [fallbackLng, "en"];
export const defaultNS = "main";
export const labels = {
ja: "日本語",
en: "English",
};
export function getOptions(lng = fallbackLng, ns = defaultNS) {
return {
// debug: true,
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: defaultNS,
defaultNS,
ns,
};
}
サーバーコンポーネント用、useTranslation
呼び出し毎に新しいインスタンスを作成します。
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { getOptions } from "./settings";
const initI18next = async (lng, ns) => {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend((language, namespace) =>
import(`./languages/${language}/${namespace}.json`)
)
)
.init(getOptions(lng, ns));
return i18nInstance;
};
export async function useTranslation(lng, ns, options = {}) {
const i18nextInstance = await initI18next(lng, ns);
return {
t: i18nextInstance.getFixedT(
lng,
ns,
// options.keyPrefix
),
i18n: i18nextInstance,
};
}
クライアントコンポーネント用
"use client";
import i18next from "i18next";
import {
initReactI18next,
useTranslation as useTranslationOrg,
} from "react-i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { getOptions } from "./settings";
i18next
.use(initReactI18next)
.use(
resourcesToBackend((language, namespace) =>
import(`./languages/${language}/${namespace}.json`)
)
)
.init(getOptions());
export function useTranslation(lng, ns, options) {
if (i18next.resolvedLanguage !== lng) i18next.changeLanguage(lng);
return useTranslationOrg(ns, options);
}
言語ファイルを 指定した場所に lng/ns.json (ja/main.json) の形で保存。
{
"hello": "hello world",
"name": "hello {{fn}} {{ln}}",
"auth": {
"login": "login"
},
"comment_zero": "zero comment",
"comment_one": "a comment",
"comment_other": "{{count}} comments",
"comment_good_other": "{{count}} good comments",
"switch": "Language switching",
"title": "Title",
"description": "Description",
"counter_zero": "zero count",
"counter_other": "{{count}} counts"
}
{
"hello": "こんにちは",
"name": "こんにちは {{fn}} {{ln}}",
"auth": {
"login": "ログイン"
},
"comment_zero": "ゼロコメ",
"comment_one": "イチコメ",
"comment_other": "{{count}} コメンツ",
"comment_good_other": "{{count}} グッド コメンツ",
"switch": "言語切替",
"title": "タイトル",
"description": "説明",
"counter_zero": "ゼロ カウント",
"counter_other": "{{count}} カウント"
}
言語切替
言語切替もサーバーコンポーネント、クライアントコンポーネント用を作成します。
サーバーコンポーネント用
import Link from "next/link";
import { useTranslation } from "../../i18n";
import { labels } from "../../i18n/settings";
export const LngButton = async ({ lng }) => {
const { t } = await useTranslation(lng);
return (
<>
<p>{t("switch")}</p>
{Object.keys(labels).map((label) => (
<Link key={label} href={`/${label}`}>
<button disabled={lng === label}>{labels[label]}</button>
</Link>
))}
</>
);
};
クライアントコンポーネント用
'use client'
import { useTranslation } from '../../i18n/client'
import { labels } from "../../i18n/settings";
import Link from 'next/link'
import { usePathname } from "next/navigation";
export const LngButton = ({ lng }) => {
const { t } = useTranslation(lng)
const path = usePathname();
const getLink = (label) => {
let reg = new RegExp(Object.keys(labels).join("|"));
let np = path.replace(reg, label);
return np;
}
return (
<>
<p>{t("switch")}</p>
{Object.keys(labels).map((label) => (
<Link key={label} href={getLink(label)}>
<button disabled={lng === label}>{labels[label]}</button>
</Link>
))}
</>
);
}
Middleware
ミドルウェアでは http://localhost:3000 にアクセスした場合、http://localhost:3000/en or ja へのリダイレクト、cookie に言語設定を保存を行います。
import { NextResponse } from "next/server";
import acceptLanguage from "accept-language";
import { fallbackLng, languages } from "./i18n/settings";
acceptLanguage.languages(languages);
export const config = {
// matcher: "/:lng*",
matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"],
};
const cookieName = "i18next";
export function middleware(req) {
let lng;
//cookieに保存されている言語を取得
if (req.cookies.has(cookieName))
lng = acceptLanguage.get(req.cookies.get(cookieName).value);
// ブラウザの設定言語
if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
//settings.js で設定した言語
if (!lng) lng = fallbackLng;
// http://localhost:3000 または、予想外の言語でアクセスされた場合にリダイレクト
if (
!languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
!req.nextUrl.pathname.startsWith('/_next')
) {
return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
}
// cookie に言語をセット
if (req.headers.has("referer")) {
const refererUrl = new URL(req.headers.get("referer"));
const lngInReferer = languages.find((l) =>
refererUrl.pathname.startsWith(`/${l}`)
);
const response = NextResponse.next();
if (lngInReferer) response.cookies.set(cookieName, lngInReferer);
return response;
}
return NextResponse.next();
}
言語検出
言語の検出は、url のパラメータを使用します。http://localhost:3000/ja なら日本語、 http://localhost:3000/en なら英語を適応させます。
import { dir } from "i18next";
import { languages } from "../../i18n/settings";
export async function generateStaticParams() {
return languages.map((lng) => ({ lng }));
}
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children, params: { lng } }) {
return (
<html lang={lng} dir={dir(lng)}>
<body>{children}</body>
</html>
);
}
サーバーコンポーネント (http://localhost:3000/en or ja )
2, 3: サーバーコンポーネント用の i18next の初期化、言語切替ボタンを読み込んで使用します。
import Link from "next/link";
import { useTranslation } from "../../i18n";
import { LngButton } from "../../components/LngButton";
// metadataを多言語対応
export async function generateMetadata({params: {lng}}){
const {t} = await useTranslation(lng, 'main')
return {
title: t("title"),
description: t("description")
}
}
export default async function Home({ params: { lng } }) {
const {t} = await useTranslation(lng, 'main')
return (
<main>
<h1>{lng}</h1>
<Link href={`/${lng}/client-page`}>client-page</Link>
<br/>
<LngButton lng={lng} />
<br />
{/* 多言語のサンプル */}
<p>{t("hello")}</p>
<p>{t("hello", { lng: "ja" })}</p>
<p>{t("auth.login")}</p>
{/* 代入 */}
<p>{t("name", { fn: "Tanaka", ln: "Taro" })}</p>
{/* 複数 */}
<p>{t("comment", { count: 0 })}</p>
<p>{t("comment", { count: 1 })}</p>
<p>{t("comment", { count: 100 })}</p>
{/* context */}
<p>{t("comment", { context: "good", count: 20 })}</p>
</main>
);
}
クライアントコンポーネント(http://localhost:3000/client-page/en or ja)
3,4: クライアントコンポーネントでは、クライアントコンポーネント用の i18next の初期化、言語切替ボタンを使用します。
'use client'
import Link from "next/link";
import { LngButton } from "../../../components/LngButton/client";
import { useTranslation } from "@/i18n/client";
export default function Client({ params: { lng } }) {
const {t} = useTranslation(lng, 'main')
return (
<main>
<h1>{lng}</h1>
<Link href={`/${lng}`}>back</Link>
<br />
<LngButton lng={lng} />
<br />
{/* 多言語のサンプル */}
<p>{t("hello")}</p>
<p>{t("hello", { lng: "ja" })}</p>
<p>{t("auth.login")}</p>
{/* 代入 */}
<p>{t("name", { fn: "Tanaka", ln: "Taro" })}</p>
{/* 複数 */}
<p>{t("comment", { count: 0 })}</p>
<p>{t("comment", { count: 1 })}</p>
<p>{t("comment", { count: 100 })}</p>
{/* context */}
<p>{t("comment", { context: "good", count: 20 })}</p>
</main>
);
}
以上です。