PR

Next.jsによる多言語対応サイトの作り方(App Router 対応)

Next.jsサイトの多言語切替対応 Web開発
記事内に広告が含まれています。
スポンサーリンク

 この記事では,Next.jsのApp Routerで多言語対応を実装する方法についてハンズオン形式でご紹介します.
前回の記事で作成したサイトをベースに実装していきます.

スポンサーリンク
スポンサーリンク

完成イメージ

 以下のようにボタン一つで日本語と英語を切り替える機能の実装を目標とします.(ブラウザに搭載されている翻訳機能を利用するのではなく,自分で日本語と英語の翻訳スクリプトを準備する方式です.)

Next.jsでの多言語対応(翻訳・切替)の様子
スポンサーリンク

ライブラリのインストール

 恒例のライブラリインストールから行います.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>
  );
}

スポンサーリンク
スポンサーリンク
Web開発
シェアする
しばをフォローする
タイトルとURLをコピーしました