この記事では,Next.jsのApp Routerで多言語対応を実装する方法についてハンズオン形式でご紹介します.
前回の記事で作成したサイトをベースに実装していきます.
完成イメージ
以下のようにボタン一つで日本語と英語を切り替える機能の実装を目標とします.(ブラウザに搭載されている翻訳機能を利用するのではなく,自分で日本語と英語の翻訳スクリプトを準備する方式です.)
ライブラリのインストール
恒例のライブラリインストールから行います.Next.js(App Router)での多言語対応は,「next-intl – Internationalization (i18n) for Next.js」というライブラリを使って実装します.
npm install next-intlファイル構成の変更
「http://localhost:3000/page-name/ja/」のように,URLにロケール情報が加わるため,ルーティングを変更する必要があります.Next.jsのApp Routerでは,ルーティングがファイル(ディレクトリ)構成によって定義されるため,ファイル構成に変更が生じます.
サイト(アプリ)作成の初期段階で,多言語対応をするか否かを判断し,必要があれば早期に実装しておくことをオススメします.
ある程度ファイルが出来た段階で実装すると,影響が大きく(パス修正等が大変に)なる可能性があります.
はじめに,src > app 以下にある全ファイルを src > app > [locale] 以下に移動させます.
そして,プロジェクトファイルの構成を以下の形式に合わせます.
├── messages(作成)
│ ├── en.json(作成)
│ └── ja.json(作成)
├── next.config.mjs(変更)
└── src
├── i18n(作成)
│ ├── routing.ts(作成)
│ └── request.ts(作成)
├── middleware.ts(無ければ作成)
└── app
└── [locale]
├── layout.tsx(移動)
└── page.tsx(移動) messages/${locale}.json
${locale}.jsonが翻訳スクリプトを定義しておくファイルになります.従って,対応する言語の数だけJSONファイルが必要です.
今回は,日本語と英語の2言語対応を実装するので,ja.jsonとen.jsonの2ファイルを作成します.
ja.json
{
"Top": {
"Welcome to": "ようこそ !",
"my portfolio site": "私のポートフォリオサイトへ"
}
}en.json
{
"Top": {
"Show More": "Show more",
"Works": "Works"
}
}next.config.mjs
next-intlプラグインで以下のようにラップする必要があります.
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);src > i18n > routing.ts
locales で対応する言語の一覧を定義します.今回は,日本語と英語なので,[‘en’, ‘ja’] を定義しました.
import {defineRouting} from 'next-intl/routing';
import {createNavigation} from 'next-intl/navigation';
export const routing = defineRouting({
locales: ['en', 'ja'],
defaultLocale: 'en'
});
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);src > i18n > request.ts
「locale as any」にしてしまうと,ビルド時に型エラーを起こすため,「locale as ‘en’ | ‘ja’」のように明示します.
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 'en' | 'ja')) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});src > middleware.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/', '/(ja|en)/:path*']
};app > [locale] > layout.tsx
翻訳ファイル(messages)を適用するために,以下の要領でラップします.
import type { Metadata } from "next";
import { Noto_Sans_JP } from "next/font/google";
import "../globals.css";
import { ChakraProvider } from '@chakra-ui/react'
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
const notoSansJP400 = Noto_Sans_JP({
weight: '400',
display: 'swap',
preload: false,
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default async function LocaleLayout({
children,
params: { locale }
}: {
children: React.ReactNode;
params: { locale: string };
}) {
if (!routing.locales.includes(locale as 'en' | 'ja')) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale}>
<body
className={notoSansJP400.className}
>
<NextIntlClientProvider messages={messages}>
<ChakraProvider>
{children}
</ChakraProvider>
</NextIntlClientProvider>
</body>
</html>
);
}
app > [locale] > page.tsx
'use client'
import { useTranslations } from 'next-intl';
// 他 必要なインポートを記述
export default function CallToActionWithAnnotation() {
const t = useTranslations('Top');
return (
<>
<Heading
fontWeight={600}
fontSize={{ base: '2xl', sm: '4xl', md: '6xl' }}
lineHeight={'110%'}>
{t('Welcome to')} <br />
<Text as={'span'} color={'green.400'}>
{t('my portfolio site')}
</Text>全行を載せるとあまりにも長くなってしまうので,要点だけを載せています.
next-intlのインポートを行った後,ラップするための変数を const t = useTranslations(‘Top’); で定義しています.
そして,本文を {t(‘Welcome to’)} のようにラップすることで,多言語対応が実装されます.
現在のロケールに応じて,ja.json または en.json のいずれかが読み込まれます.
Tips
以上が公式ドキュメント(App Router setup with i18n routing – Internationalization (i18n) for Next.js)の要約になります.
せっかくなので,公式ドキュメント外の実用的な機能を実装していきます.
ロケールスイッチ
言語を切り替えるUIと機能を実装していきます.URLに含まれるロケールで言語を識別しているため,言語を切り替えるにはURLを切り替える必要があります.
以下のように正規表現を用いた置き換えをすることで,柔軟なロケールスイッチを実装することができます.
import { useRouter } from 'next/navigation';
import { useLocale } from 'next-intl';
export default function LanguageSwitcher() {
const router = useRouter();
const currentLocale = useLocale();
const switchLocale = (newLocale: string) => {
const pathname = window.location.pathname;
const newPath = pathname.replace(
new RegExp(`\\b(${currentLocale})\\b`, 'g'),
newLocale
);
return newPath;
};
return (
<div className="flex space-x-2">
<div className="font-semibold transition-all">
<span
onClick={() => router.push(switchLocale('en'))}
className={`cursor-pointer ${currentLocale === 'en' ? 'text-blue-500' : 'text-black'}`}
>
English
</span>
<span className="mx-2">/</span>
<span
onClick={() => router.push(switchLocale('ja'))}
className={`cursor-pointer ${currentLocale === 'ja' ? 'text-blue-500' : 'text-black'}`}
>
日本語
</span>
</div>
</div>
);
}

