🌎

Next.js で i18n 対応するまで

2024/11/15に公開

はじめに

数年前から独学でプログラミングを学び、最近は個人開発をしている鈴木です。

今回は今開発している PlaylistManager というプロジェクトで、i18n対応するために必要だったことなどをまとめます。
実際の実装に関するコミットは以下の二つです:

前提

  1. すでにあるプロジェクトの i18n 対応なので、破壊的で大きな変更はしたくなかった。
  2. 言語を変更したときに、ほかのコンポーネントの状態は維持したいので、ページ全体のリロードを必要としない。

実装方針

  • 言語は 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 のスターもよろしくお願いします!
https://github.com/suzuki3jp/PlaylistManager
https://zenn.dev/suzuki3jp/articles/nextauth-approuter-google-20241025

GitHubで編集を提案

Discussion