Next.js で i18n 対応するまで
はじめに
数年前から独学でプログラミングを学び、最近は個人開発をしている鈴木です。
今回は今開発している PlaylistManager というプロジェクトで、i18n対応するために必要だったことなどをまとめます。
実際の実装に関するコミットは以下の二つです:
前提
- すでにあるプロジェクトの i18n 対応なので、破壊的で大きな変更はしたくなかった。
- 言語を変更したときに、ほかのコンポーネントの状態は維持したいので、ページ全体のリロードを必要としない。
実装方針
- 言語は URLに含まれているクエリ(
?lang=ja
)で決定する- パスに言語を含める方法(
/[lang]/path/to/resource
)もあるようだが、前提1を考慮してクエリに決定。
- パスに言語を含める方法(
- 言語変更は SelectMenu で実装
実装
必要なライブラリのインストール
今回のプロジェクトでは pnpm
を使用しているので pnpm
でインストールします。
pnpm add react-i18next i18next i18next-resources-to-backend
next-i18next
の設定
今回は src/locales/[lang]/
配下にそれぞれの言語のJSONファイルを配置することにします。
{
"title": "Sample title"
}
{
"title": "サンプルタイトル"
}
// これは 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に含まれるクエリから言語を決定するフックを作成する
入力されたクエリから安全に言語を決定する関数
// ...
export const getSafeLang = (lang: any) => {
return typeof lang === "string" && AVAILABLE_LANGUAGES.includes(lang)
? lang
: DEFAULT_LANGUAGE;
};
クライアントサイド用
今回は useT
というフックを作成し、その中で言語を決定し、その言語の t
関数を取得します。
"use client";
import { QUERY_NAME, getOptions, getSafeLang } 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();
const lang = getSafeLang(query.get(QUERY_NAME))
// この `common` はネームスペース名です。今回は `common.json` なので `common` を設定
// jsonファイルを増やし、ネームスペースを利用したい場合、`useT` に引数を設定しましょう
const { t, i18n } = useTranslation("common");
useEffect(() => {
i18n.changeLanguage(lang);
}, [lang, i18n]);
return { t, i18n, lang };
};
サーバーサイド用
サーバーサイドで使うものをフックというのはどうなのかというのは置いといて
import { QUERY_NAME, getOptions, getSafeLang } from "@/locales/settings";
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
// Nextjs で page.tsx の Props として受け取った searchParams の型情報
// [See](https://nextjs.org/docs/app/api-reference/functions/use-search-params#server-components)
export interface PageProps {
searchParams: Promise<{
[key: string]: string | string[] | undefined;
}>;
}
export const useServerT = async (query: PageProps["searchParams"]) => {
const lang = (await query)[QUERY_NAME];
const resolvedLang = getSafeLang(lang);
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(lang: string, namespace: string) =>
import(`@/locales/${lang}/${namespace}.json`),
),
)
.init({
...getOptions(),
lng: resolvedLang,
});
return {
t: i18nInstance.t,
i18n: i18nInstance,
lng: i18nInstance.language,
};
};
翻訳を利用する
import { useT } from "@/hooks/useT.ts"; // クライアントサイドならこっち
import { useServerT } from "@/hooks/useServerT.ts"; // サーバーサイドならこっち
export default function Home() {
const { t } = useT();
return <h1>{t("title")}</h1> // "サンプルタイトル" か "Sample title" が言語設定に応じて表示されます
}
JSONファイル内でのネストにも対応しています。
{
"metadata": {
"title": "Sample title",
"description": "Sample description"
}
}
{t("metadata.title")}
{t("metadata.description")}
動的な値を翻訳に利用することもできます。
{
"hi": "Hi! {{name}}!"
}
{t("hi", { name: "SUZUKI" })} // "Hi! SUZUKI!"
HTML タグを仕込むこともできます
{
// 1, 2 などのタグ名は任意 <hogehoge></hogehoge> とかでも可
"hi": "Hi! <1>{{firstLink}}</1> <2>{{secondLink}}</2>!"
}
import { Trans } from "react-i18next";
<Trans
i18nKey="hi"
values={{ firstLink: "first link", secondLink: "second link" }}
components={{
1: <Link href="https://example.com" />,
2: <Link href="https://example.com" />,
}}
/>
// first link と second link が Link に包まれてレンダリングされる
言語を変更する
ここでは言語を変更する関数のみを紹介します。
これをボタンなりセレクトメニューなりの 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()}`) // リロードなしで再レンダリングされる(たぶん)
}
Trans
コンポーネントを使う方法
[補足] サーバーサイドで 結論 TransWithoutContext
コンポーネントを使用する
import { Trans as TransWithoutContext } from "react-i18next/TransWithoutContext";
// 使い方は前述の `Trans` と同じ
解説
src/context.js
で createContext がトップレベルで呼ばれており、それが index.js に読み込まれているため、
import { Trans as TransWithoutContext } from "react-i18next";
すると context.js
が読み込まれるためエラーが出ます。
さいごに
今回のプロジェクトはまだ開発中で説明など静的なページが全くなく、すべてが状態によって変化する動的なページでした。
そのため、クライアントコンポーネントのみのi18n対応だったので、今後サーバーコンポーネントでもi18n対応する必要があったら、追記しようと思います 追記しました。
よければ他の記事、PlaylistManager
のスターもよろしくお願いします!
Discussion