はじめに
こんにちは、Gakken LEAPのフロントエンドエンジニアの Okuma です。
今回はNext.jsのApp Routerでi18n(国際化)の対応をしてみたので、その際の手順やtipsについてご紹介できればと思います。
i18nとは、Internationalizationの略でソフトウェアや製品をさまざまな言語や地域に適用させることを意味しており、これには、テキストの翻訳、通貨や日付の表示形式の調整、文化的な違いを考慮したインターフェースの設計などが含まれるそうです。i18n(国際化)の対応を行うことで、世界中のユーザーが母国語もしくは共通語でソフトウェアや製品にアクセスして利用することができるようになります。
読み方は「アイじゅうはちエヌ」です。
Next.jsではデフォルトでApp RouterでもPage Routerでもi18nがビルトインでサポートされています。(公式ドキュメント)
ただ、今回はデフォルトロケールについてはパスにロケールを含めたくなかったため、簡単に実装できる next-intl を使用して実装してみました。
同じようにi18nの実装で困っている方の助けになればと思います!
セットアップするにあたっての注意点
公式ドキュメントのGet Startedのページを覗いてみると、App RouterとPage Router、さらにApp Routerの中でも「With i18n routing」と「Without i18n routing」があります。
「Without i18n routing」で実装してしまうと単一の言語のみの対応となってしまうので、複数言語に対応したい場合は「With i18n routing」を選択しましょう。
今回はApp Routerかつ、複数言語(日本語、英語)に対応したいと思っているので、「With i18n routing」の手順に沿って進めていきます!
セットアップ手順
基本的には公式ページの手順で問題ありませんが、日本語ドキュメントがありませんのである程度噛み砕いた形にして、さらに詰まったポイントについても解説を加えてご紹介できればと思います。
App Routerのプロジェクトがない場合は、以下のコマンドで生成できます。
npx create-next-app@latest
すでにプロジェクトがある場合は以下のコマンドでnext-intl
をインストールします。
npm install next-intl
最終的なディレクトリ・ファイル構造は以下のようになります。
├── messages
│ ├── en.json (1)
│ └── ja.json (1)
├── next.config.mjs (2)
└── src
├── i18n
│ ├── routing.ts (3)
│ └── request.ts (5)
├── middleware.ts (4)
└── app
└── [locale]
├── layout.tsx (6)
└── page.tsx (7)
既存のApp Routerのディレクトリ構造がある場合は、[locale]
配下にページファイルがすべて収まるように移動させます。
(1) messages/en.json
messages
ディレクトリ以下に翻訳したい言語ごとのJSONファイルを配置します。もちろんリモートのデータソースから読み込むこともできますが、今回はローカルで読み込む一番簡単な方法で進めます。
複数言語に対応したい場合は、その言語の数だけJSONファイルが必要になります。
{
"home": {
"title": "Hello World!",
"about": "Go to the about page"
}
}
{
"home": {
"title": "こんにちは、世界!",
"about": "aboutページへ行く"
}
}
言語別のJSONファイルを用意したとしても、キーはすべて揃えなければなりません。各言語ファイルの同じキーにある文字列を引っ張ってきて、表示を切り替えることになります。
(2) next.config.js
サーバーコンポーネントにリクエスト固有のi18nを提供するためのエイリアスを作成するプラグインを設定します。
const createNextIntlPlugin = require('next-intl/plugin');
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = withNextIntl(nextConfig);
すでにnext.config.js
で何かしらの設定を記載している場合は、const nextConfig = {}
の中に移行すれば問題ありません。
(3) src/i18n/routing.ts
次の2つの箇所でルーティングを統合します。
- ミドルウェア:ロケールを判定し、リダイレクト・リライトを処理する(例.
/
→/en
) - ナビゲーションAPI:Next.jsのナビゲーションAPIの軽量ラッパー
<Link />
これによって/about
のようなパスの操作ができるようになります。言語プレフィックスなどのi18nの機能は裏側で処理されます。
routing.ts
を作成して、この2つの場所で設定を共有します。
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
export const routing = defineRouting({
// A list of all locales that are supported
locales: ['en', 'ja'],
// Used when no locale matches
defaultLocale: 'en'
});
// Lightweight wrappers around Next.js' navigation APIs
// that will consider the routing configuration
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);
(4) src/middleware.ts
先ほど作成したルーティング設定をミドルウェアで使用していきます。
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(de|en)/:path*']
};
(5) src/i18n/request.ts
サーバーコンポーネントでnext-intl
の機能を使用する場合、関連する設定はi18n/request.ts
に配置されている中央モジュールから読み取られます。この設定のスコープは現在のリクエストに限定されており、ユーザーのロケールに基づいてメッセージやその他のオプションを提供するために使用できます。
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({requestLocale}) => {
// This typically corresponds to the `[locale]` segment
let locale = await requestLocale;
// Ensure that a valid locale is used
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
(6) src/app/[locale]/layout.tsx
ミドルウェアによって一致したロケールは、locale
パラメータを介して利用でき、それをドキュメント言語として使用できます。さらにNextIntlClientProvider
でラップすることで、i18n/request.ts
からクライアントコンポーネントに設定を渡すことができるようになります。
ここまで設定すると、サーバーコンポーネントでもクライアントコンポーネントでもi18nの機能を使えるようになります。
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
export default async function LocaleLayout({
children,
params: {locale}
}: {
children: React.ReactNode;
params: {locale: string};
}) {
// Ensure that the incoming `locale` is valid
if (!routing.locales.includes(locale as any)) {
notFound();
}
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
(7) src/app/[locale]/page.tsx
ここまで実装できれば各ページやコンポーネントでnext-intl
の機能を使って翻訳することができます。
import { useTranslations } from 'next-intl';
import { Link } from '@/i18n/routing';
export default function HomePage() {
const t = useTranslations('home');
return (
<div>
<h1>{t('title')}</h1>
<Link href="/about">{t('about')}</Link>
</div>
);
}
実装の注意点やTips
いくつか実装した際に詰まったポイントや、取り入れて良かった設定などをご紹介します。
デフォルトロケールの取り扱い
実はi18nの機能はNext.jsの標準でも搭載されていますが、今回next-intl
を選んだのはデフォルトロケールの取り扱い方が簡単だったためです。
というのは、ここまでの実装だとパスにロケールが入った状態がデフォルトになります。ルートであれば、/en/
、/ja/
になります。
私の場合は英語をデフォルトにし、さらにプレフィックスを付けたくなかったので、ロケールがen
の場合は/
にして、日本語の場合のみパスに/ja/
挟む形にしたかったです。
設定自体は簡単で、src/i18n/routing.ts
を以下のようにします。
import { createNavigation } from 'next-intl/navigation';
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'ja'],
defaultLocale: 'en',
// ↓この設定
localePrefix: 'as-needed',
});
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
これで/
(英語)と/ja
(日本語)のルーティングが可能になります。
設定値のデフォルトがalways
ですが、これにするとプレフィックスが毎回付くようになります。
middlewareに既存実装がある場合
手順の中でsrc/middleware.ts
の実装がありましたが、これは新規実装向けになっています。私がnext-intl
を導入したのはすでに既存実装がある状態でした。その場合は手順通りだとうまくいかないことがあるので、以下を参考にしました。
Composing other middleware
import createMiddleware from 'next-intl/middleware';
import { NextRequest } from 'next/server';
export default async function middleware(request: NextRequest) {
// Step 1: Use the incoming request (example)
const defaultLocale = request.headers.get('x-your-custom-locale') || 'en';
// Step 2: Create and call the next-intl middleware (example)
const handleI18nRouting = createMiddleware({
locales: ['en', 'de'],
defaultLocale
});
const response = handleI18nRouting(request);
// Step 3: Alter the response (example)
response.headers.set('x-your-custom-locale', defaultLocale);
return response;
}
...
この実装ではNext.jsの標準のmiddleware
関数を使用しています。私の場合ミドルウェアでカスタムヘッダの追加などを行っていたので、next-intl
のcreateMiddleware
との統合が必須でした。
その場合、middleware
メソッド内でcreateMiddleware
を呼び出し、一旦変数に格納します。そして引数のrequest
をラップしてこれを返してあげることで、標準のmiddleware
メソッドと統合してもnext-intl
のミドルウェアとしても機能するようになります。
おわりに
今回はnext-intl
を使ったApp Routerでのi18n(国際化)対応について、最短実装の手順と注意点・Tipsについて簡単に紹介させていただきました。
現在動いているプロジェクトではPage Routerでi18nを対応しているため、今後App Routerへの移行などの際に参考にしたいと思います。
エンジニア募集中
Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています!!
ぜひお気軽にカジュアル面談へお越しください!!