Next.js App Routerでのi18n対応全部調べてみた
こんにちは、エンジニアの籏野です。
この度、Next.jsのApp Routerを利用したアプリケーションへのi18nの導入方法を調査することになりました。
Pages Routerを利用した場合の導入方法はイメージがつくのですがApp Routerを利用した場合は初めてとなりますので、その方法を調査・比較しました。
なお今回の調査は主に、Localizationの方法に焦点を当てています。
言語毎の出し分けのためのルーティング方法についてはそこまで解説しないのでご了承ください。
Next.jsの公式ドキュメントのinternationalizationページでは、いくつかLocalizationの方法が紹介されています。
このうち、react-i18n-routerはreact-i18nextと組み合わせて利用していること、またparaglide-nextはNext.jsとの連携が非推奨に見えることから以下の方法を調査していきます。
- Minimalな方法
- next-intl
- next-international
- react-i18next
- lingui
各手法の比較
言語ファイルについて
Minimal | next-intl | next-international | react-i18next | lingui | |
---|---|---|---|---|---|
言語ファイルの管理方法 | JSON/TS | JSON/TS | JSON/TS | JSON | .po/JSONが自動生成される |
言語ファイルの分割読み込みが可能か | ❌ | ❌ | ❌ | ✅ | △ experimentalな機能で可能 |
機能面の比較
Minimal | next-intl | next-international | react-i18next | lingui | |
---|---|---|---|---|---|
interpolation | ❌ | ✅ | ✅ | ✅ | ✅ |
pluralization | ❌ | ✅ | ✅ | ✅ | ✅ |
format | ❌ | ✅ | ❌ | ✅ | ✅ |
HTMLタグの埋め込み | ❌ | ✅ | ❌ | ✅ | ✅ |
コンポーネント外での利用 | ✅ | ❌ | ❌ | ✅ | ✅ |
※ pluralizationは入力値に応じて単数形/複数形を切り替えるような機能です。
各手法の詳細
Minimalな方法
設定・導入方法
こちらは特にライブラリをインストールせずにLocalizationを実現する方法です。
JSONファイルで言語ファイルを用意し、指定した言語のJSONファイルをimportするというシンプルな方法です。
import "server-only";
// Localeは`"en" | "ja"`のような取り得る言語の型になっています
import type { Locale } from "@/i18n-config";
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();
利用方法
言語毎のテキストはJSONオブジェクトとして取得できますので、以下のように利用したいテキストを指定しコンポーネントに埋め込みます。
シンプルゆえにinterpolation等の機能はありませんが、最低限の機能は実現できています。
import type { Locale } from "../../../i18n-config";
import LocaleSwitcher from "../components/locale-switcher";
import Counter from "./components/counter";
import { getDictionary } from "./i18n/get-dictionary";
export default async function IndexPage(props: {
params: Promise<{ lang: Locale }>;
}) {
const { lang } = await props.params;
const dictionary = await getDictionary(lang);
return (
<div>
<div>
<p>{dictionary["server-component"].welcome}</p>
<Counter dictionary={dictionary.counter} />
</div>
<LocaleSwitcher />
</div>
);
}
next-intl
設定・導入方法
以下のコマンドにてnext-intlをインストールします。
pnpm add next-intl
Next.jsでの利用にあたっては以下のような設定ファイルが必要です。
import { hasLocale } from "next-intl";
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async () => {
// read from `cookies()`, `headers()`, etc.
const locale = 'en';
return {
locale,
messages: (await import(`./dictionaries/${locale}.json`)).default,
};
});
この設定ファイルはnext.config.tsから読み込みます。
import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {
/* config options here */
};
const withNextIntl = createNextIntlPlugin(
"path-to-request-config"
);
export default withNextIntl(nextConfig);
TypeScript利用時は型が効くようにするのがよいでしょう
import type messages from "path-to-json";
declare module "next-intl" {
interface AppConfig {
Messages: typeof messages;
}
}
またlayout.tsxにてNextIntlClientProvider
を読み込む必要があります。
import { NextIntlClientProvider } from "next-intl";
export default async function LocaleLayout({
children,
}: {
children: React.ReactNode;
}) {
return <NextIntlClientProvider>{children}</NextIntlClientProvider>;
}
利用方法
サーバーコンポーネント/クライアントコンポーネントで利用方法が異なります。
サーバーコンポーネント
import { getTranslations } from "next-intl/server";
export default async function Page() {
const t = await getTranslations("context");
return (
...
<p>{t("welcome")}</p>
...
);
}
クライアントコンポーネント
"use client";
import { useTranslations } from "next-intl";
export default function ClientComponent() {
const t = useTranslations("context");
return (
...
<p>{t("welcome")}</p>
...
);
}
その他機能
interpolation
// "message": "Hello {name}!"
t('message', {name: 'Jane'}); // "Hello Jane!"
pluralization
// "message": "You have {count, plural, =0 {no followers yet} =1 {one follower} other {# followers}}."
t('message', {count: 0}); // "You have no followers yet"
t('message', {count: 1}); // "You have one follower"
t('message', {count: 3580}); // "You have 3,580 followers"
format
Intlオブジェクトを利用しているようです。
HTMLタグの埋め込み
// "message": "Please refer to <guidelines>the guidelines</guidelines>."
t.rich('message', {
guidelines: (chunks) => <a href="/guidelines">{chunks}</a>
});
コンポーネント外での利用
サーバーサイドでのみ可能ですが、基本的には推奨していないようです。
next-intl is heavily based on the useTranslations API which is intended to consume translations from within React components. This may seem like a limitation, but this is intentional and aims to encourage the use of proven patterns that avoid potential issues—especially if they are easy to overlook.
next-international
設定・導入方法
以下のコマンドにてnext-internationalをインストールします。
pnpm add next-international
JSONで言語ファイルを用意したら、クライアント/サーバーそれぞれのための設定ファイルを作成します。
"use client";
import { createI18nClient } from "next-international/client";
export const { useI18n, useScopedI18n, I18nProviderClient } = createI18nClient({
en: () => import("./dictionaries/en"),
ja: () => import("./dictionaries/ja"),
});
import { createI18nServer } from "next-international/server";
export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({
en: () => import("./dictionaries/en"),
ja: () => import("./dictionaries/ja"),
});
また、layout.tsxにてI18nProviderClient
を読み込む必要があります。
import type { ReactElement } from "react";
import { I18nProviderClient } from "./i18n/client";
export default async function Layout({
params,
children,
}: { params: Promise<{ lang: string }>; children: ReactElement }) {
const { lang } = await params;
return <I18nProviderClient locale={lang}>{children}</I18nProviderClient>;
}
利用方法
サーバーコンポーネント
import { getScopedI18n } from "./i18n/server";
export default async function Page() {
const t = await getScopedI18n("server-component");
return (
...
<p>{t("welcome")}</p>
...
);
}
クライアントコンポーネント
"use client";
import { useScopedI18n } from "./i18n/client";
export default function ClientComponent() {
const t = useScopedI18n("context");
return (
...
<p>{t("welcome")}</p>
...
);
}
その他機能
interpolation
// "message": "Hello {name}!"
t('message', {name: 'Jane'}); // "Hello Jane!"
pluralization
言語ファイルにて#zero
や#one
等の条件を末尾に付けることで利用可能です
// 'cows#zero': 'No cows',
// 'cows#one': 'A cow',
// 'cows#other': '{count} cows'
t('cows', { count: 0 }) // "No cows"
t('cows', { count: 1 }) // "A cow"
t('cows', { count: 3 }) // "3 cows"
format
なし
HTMLタグの埋め込み
なし
コンポーネント外での利用
なし
react-i18next
設定・導入方法
以下のコマンドにて必要なライブラリをインストールします。
pnpm add react-i18next i18next
言語ファイル及び設定は他のライブラリと似たようなものを用意しますが、namespaceという概念があることが特徴です。
これにより、全ての言語ファイルを読み込まずに、必要なものだけを読み込むことができます。
import { type Resource, createInstance, type i18n } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { i18n as i18nConfig } from "@/i18n-config";
export default async function initTranslations(
locale: string,
namespaces: string[],
i18nInstance?: i18n,
resources?: Resource,
) {
const instance = i18nInstance || createInstance();
instance.use(initReactI18next);
if (!resources) {
instance.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`./dictionaries/${language}/${namespace}.json`),
),
);
}
await instance.init({
lng: locale,
resources,
fallbackLng: i18nConfig.defaultLocale,
supportedLngs: i18nConfig.locales,
defaultNS: namespaces[0],
fallbackNS: namespaces[0],
ns: namespaces,
preload: resources ? [] : i18nConfig.locales,
});
return {
i18n: instance,
resources: { [locale]: instance.services.resourceStore.data[locale] },
t: instance.t,
};
}
また、TranslationsProvider
を用意しlayout.tsxにて読み込む必要があります。
"use client";
import { createInstance } from "i18next";
import { I18nextProvider } from "react-i18next";
import initTranslations from "./i18n/i18next";
export default function TranslationsProvider({
children,
locale,
namespaces,
}: {
children: React.ReactNode;
locale: string;
namespaces: string[];
}) {
const i18n = createInstance();
initTranslations(locale, namespaces, i18n);
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}
import type { ReactElement } from "react";
import TranslationsProvider from "./provider";
export default async function Layout({
params,
children,
}: { params: Promise<{ lang: string }>; children: ReactElement }) {
const { lang } = await params;
return (
<TranslationsProvider
locale={lang}
namespaces={[...]}
>
{children}
</TranslationsProvider>
);
}
TypeScript利用時は型が効くようにするのがよいでしょう
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "default";
resources: {
// namespace毎の型を定義
};
}
}
利用方法
サーバーコンポーネント
import initTranslations from "./i18n/i18next";
export default async function Page() {
const { t } = await initTranslations(lang, [...]);
return (
...
<p>{t("welcome")}</p>
...
);
}
クライアントコンポーネント
"use client";
import { useTranslation } from "react-i18next";
export default function ClientComponent() {
const { t } = useTranslation(...);
return (
...
<p>{t("welcome")}</p>
...
);
}
その他機能
interpolation
// "message": "Hello {name}!"
t('message', {name: 'Jane'}); // "Hello Jane!"
pluralization
言語ファイルにて、_zero
や_one
等の条件を末尾に付けることで利用可能です
// "key_one": "item"
// "key_other": "items"
t("key", { count: 1 }); // "item"
t("key", { count: 2 }); // "items"
format
Intlオブジェクトを利用しているようです。
HTMLタグの埋め込み
<Trans>
コンポーネントを利用することで可能です。
<Trans i18nKey="userMessagesUnread" count={count}>
Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>
コンポーネント外での利用
i18nインスタンスを直接扱えばいいようです。
import i18n from "./i18n/i18next";
i18n.t("key", { count: 1 });
lingui
設定・導入方法
以下のコマンドで必要なライブラリをインストールします。
pnpm add @lingui/core @lingui/react
pnpm add -D @lingui/cli @lingui/swc-plugin
CLI用に設定ファイルを用意します。
import { defineConfig } from "@lingui/cli";
import { formatter } from '@lingui/format-json';
import {i18n} from "./src/i18n-config"
export default defineConfig({
sourceLocale: i18n.defaultLocale,
locales: [...i18n.locales],
catalogs: [
{
path: "<rootDir>/src/...",
include: ["src"],
},
],
fallbackLocales: {
default: i18n.defaultLocale,
},
});
package.jsonにCLI用のスクリプトを追加します。
{
"scripts": {
"lingui:extract": "lingui extract --clean",
"lingui:compile": "lingui compile --typescript"
}
}
また、next.config.ts
にてswc-pluginを読み込む必要があります。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
swcPlugins: [["@lingui/swc-plugin", {}]],
},
};
export default nextConfig
また言語ファイルを読み込むための設定ファイルを用意する必要があります。
import "server-only";
import { type I18n, type Messages, setupI18n } from "@lingui/core";
import linguiConfig from "@/lingui.config";
const { locales } = linguiConfig;
// optionally use a stricter union type
type SupportedLocales = string;
async function loadCatalog(locale: SupportedLocales): Promise<{
[k: string]: Messages;
}> {
const { messages } = await import(
`./i18n/dictionaries/${locale}/messages`
);
return {
[locale]: messages,
};
}
const catalogs = await Promise.all(locales.map(loadCatalog));
// transform array of catalogs into a single object
export const allMessages = catalogs.reduce((acc, oneCatalog) => {
return { ...acc, ...oneCatalog };
}, {});
type AllI18nInstances = { [K in SupportedLocales]: I18n };
export const allI18nInstances: AllI18nInstances = locales.reduce(
(acc, locale) => {
const messages = allMessages[locale] ?? {};
const i18n = setupI18n({
locale,
messages: { [locale]: messages },
});
return { ...acc, [locale]: i18n };
},
{},
);
export const getI18nInstance = (locale: SupportedLocales): I18n => {
if (!allI18nInstances[locale]) {
console.warn(`No i18n instance found for locale "${locale}"`);
}
return allI18nInstances[locale]! || allI18nInstances["en"]!;
};
クライアント用のproviderを用意し、上記の設定をlayout.tsx/page.tsx双方で読み込む必要があります。
"use client";
import { type Messages, setupI18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { useState } from "react";
export function LinguiClientProvider({
children,
initialLocale,
initialMessages,
}: {
children: React.ReactNode;
initialLocale: string;
initialMessages: Messages;
}) {
const [i18n] = useState(() => {
return setupI18n({
locale: initialLocale,
messages: { [initialLocale]: initialMessages },
});
});
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
}
import { setI18n } from "@lingui/react/server";
import { getI18nInstance } from "./appRouterI18n";
import { LinguiClientProvider } from "./provider";
type Props = {
params: Promise<{
lang: string;
}>;
children: React.ReactNode;
};
export default async function RootLayout({ params, children }: Props) {
const { lang } = await params; // get the language from the params
const i18n = getI18nInstance(lang); // get a ready-made i18n instance for the given locale
setI18n(i18n); // make it available server-side for the current request
return (
<LinguiClientProvider initialLocale={lang} initialMessages={i18n.messages}>
{children}
</LinguiClientProvider>
);
}
import { setI18n } from "@lingui/react/server";
import { getI18nInstance } from "./appRouterI18n";
export default async function IndexPage({
params,
}: {
params: Promise<{
lang: string;
}>;
}) {
const { lang } = await params; // get the language from the params
const i18n = getI18nInstance(lang); // get a ready-made i18n instance for the given locale
setI18n(i18n); // make it available server-side for the current request
return (
...
);
}
利用方法
翻訳作業の大まかな流れは以下のようになります。
- 翻訳したい箇所を
<Trans>
コンポーネントで囲む
<Trans>Welcome</Trans>
-
pnpm run lingui:extract
を実行し、翻訳箇所を言語ファイルに書き出す。 - 翻訳した内容を言語ファイルに記載する。
-
pnpm run lingui:compile
を実行し、言語ファイルをコンパイルする。- コンパイルしたファイルはtsファイルとして出力されるので、これを
appRouterI18n.ts
が読み込む
- コンパイルしたファイルはtsファイルとして出力されるので、これを
他のライブラリが先に言語ファイルを用意する必要があるのに対し、lingui
は言語ファイルがソースコードから自動で生成されるというのが大きな違いです。
その他機能
<Trans>
コンポーネントを利用していると、以下の機能は不要なように感じました。
そこで以下の機能をコンポーネント外で利用する方法をまとめます。
interpolation
コンポーネントの場合は、通常のコンポーネントと同様に値を埋めこんでいきます。
<Trans>Hello {name}!</Trans>
コンポーネント外では、テンプレートリテラルのように埋め込むことができます。
import { t } from "@lingui/core/macro";
const name = "Tom";
t`My name is ${name}`;
t({ id: "msg.name", message: `My name is ${name}` });
pluralization
Pluralコンポーネントもしくはpluralマクロを使います
import { Plural } from "@lingui/react/macro";
<Plural value={numBooks} one="Book" other="Books" />;
import { plural } from "@lingui/core/macro";
const message = plural(numBooks, {
one: "# book",
other: "# books",
});
format
Intlオブジェクトを利用しているようです。
HTMLタグの埋め込み
<Trans>
コンポーネントを利用することで通常のReactコンポーネントと同じ感覚で利用可能です。
コンポーネント外での利用
@lingui/core/macro
のt関数を利用します。
t`Message`;
t({
id: "custom.id",
message: "Message with custom ID",
});
まとめ
今回の記事では、App Routerで利用可能なi18nライブラリを紹介しました。
linguiはマクロを用いた言語ファイルの自動生成を行っており、他のライブラリとは大きく異なるのが印象的でした。
今回の調査結果が皆さんのライブラリ選定の参考になれば幸いです。
この記事を書いた人
籏野 拓
2018年新卒入社
Discussion