🌎
Next.js で i18n 対応するまで
はじめに
数年前から独学でプログラミングを学び、最近は個人開発をしている鈴木です。
今回は今開発している PlaylistManager というプロジェクトで、i18n対応するために必要だったことなどをまとめます。
実際の実装に関するコミットは以下の二つです:
前提
- すでにあるプロジェクトの i18n 対応なので、破壊的で大きな変更はしたくなかった。
- 言語を変更したときに、ほかのコンポーネントの状態は維持したいので、ページ全体のリロードを必要としない。
実装方針
- 言語は URLに含まれているクエリ(
?lang=ja
)で決定する- パスに言語を含める方法(
/[lang]/path/to/resource
)もあるようだが、前提1を考慮してクエリに決定。
- パスに言語を含める方法(
- 言語変更は SelectMenu で実装
実装
必要なライブラリのインストール
今回のプロジェクトでは pnpm
を使用しているので pnpm
でインストールします。
pnpm add next-i18next react-i18next i18next i18next-resources-to-backend
next-i18next
の設定
今回は src/locales/[lang]/
配下にそれぞれの言語のJSONファイルを配置することにします。
src/locales/en/common.json
{
"title": "Sample title"
}
src/locales/ja/common.json
{
"title": "サンプルタイトル"
}
src/locales/settings.ts
// これは URL に含まれるどのクエリを言語設定用にするかの項目です(今回は `?lang=en`)
export const QUERY_NAME = "lang";
// ここで指定する文字列は `src/locales/[lang]/` の lang に一致している必要があります。
export const DEFAULT_LANGUAGE = "ja";
export const AVAILABLE_LANGUAGES = [DEFAULT_LANGUAGE, "en"];
export const getOptions = (lang: string = DEFAULT_LANGUAGE) => {
return {
lng: lang,
fallbackLng: DEFAULT_LANGUAGE,
supportedLngs: AVAILABLE_LANGUAGES,
};
};
URLに含まれるクエリから言語を決定するフックを作成する
今回は useT
というフックを作成し、その中で言語を決定し、その言語の t
関数を取得します。
src/hooks/useT.ts
"use client";
import { DEFAULT_LANGUAGE, QUERY_NAME, getOptions } from "@/locales/settings";
import i18next from "i18next";
import resourceToBackend from "i18next-resources-to-backend";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { initReactI18next, useTranslation } from "react-i18next";
i18next
.use(initReactI18next)
.use(
resourceToBackend(
(lang: string, namespace: string) =>
import(`@/locales/${lang}/${namespace}.json`),
),
)
.init(getOptions());
export const useT = () => {
const query = useSearchParams();
// lang クエリが含まれていない場合 `DEFAULT_LANGUAGE` が使用されます
let lang = query.get(QUERY_NAME) || DEFAULT_LANGUAGE;
lang = AVAILABLE_LANGUAGES.includes(lang) ? lang : DEFAULT_LANGUAGE;
// この `common` はネームスペース名です。今回は `common.json` なので `common` を設定
// jsonファイルを増やし、ネームスペースを利用したい場合、`useT` に引数を設定しましょう
const { t, i18n } = useTranslation("common");
useEffect(() => {
i18n.changeLanguage(lang);
}, [lang, i18n]);
return { t, i18n, lang };
};
翻訳を利用する
src/app/page.tsx
import { useT } from "@/hooks/useT.ts"
export default function Home() {
const { t } = useT();
return <h1>{t("title")}</h1> // "サンプルタイトル" か "Sample title" が言語設定に応じて表示されます
}
JSONファイル内でのネストにも対応しています。
src/locales/en/common.json
{
"metadata": {
"title": "Sample title",
"description": "Sample description"
}
}
{t("metadata.title")}
{t("metadata.description")}
動的な値を翻訳に利用することもできます。
src/locales/en/common.json
{
"hi": "Hi! {{name}}!"
}
{t("hi", { name: "SUZUKI" })} // "Hi! SUZUKI!"
言語を変更する
ここでは言語を変更する関数のみを紹介します。
これをボタンなりセレクトメニューなりの onClick
などによしなに渡してください。
実際のセレクトメニューでの実装例は このコミット にあります。
"use client";
import type { AVAILABLE_LANGUAGES, QUERY_NAME } from "@/locales/settings.json";
import { useRouter } from "next/navigation";
const changeLang = (newLang: AVAILABLE_LANGUAGES) => {
const router = useRouter();
const newParams = new URLSearchParams();
params.set(QUERY_NAME, newLang);
router.push(`?${params.toString()}`) // リロードなしで再レンダリングされる(たぶん)
}
さいごに
今回のプロジェクトはまだ開発中で説明など静的なページが全くなく、すべてが状態によって変化する動的なページでした。
そのため、クライアントコンポーネントのみのi18n対応だったので、今後サーバーコンポーネントでもi18n対応する必要があったら、追記しようと思います。
よければ他の記事、PlaylistManager
のスターもよろしくお願いします!
Discussion