👏

Next.js v13でのi18n対応を試してみた

2023/10/13に公開

概要

以下ポストでNext.js v13のi18n対応が紹介されていたため、実際に試してみました。
https://twitter.com/asidorenko_/status/1690416384486838273

1. 多言語対応用のJSONを作成

srcフォルダ直下にdictionariesを作成し、ja.jsonen.jsonを置く。

ja.json
{
  "hello": {
    "title": "こんにちは、世界",
    "text": "Next.jsを使ったi18n対応"
  }
}
en.json
{
  "hello": {
    "title": "Hello World!",
    "text": "i18n support using Next.js"
  }
}

2. 言語に応じたjsonをインポートする関数を定義

jsonファイルと同じ階層に渡された言語に応じたjsonをインポートするための関数を定義する。

index.ts
import 'server-only';

// 辞書の型を定義
type Dictionary = {
  [key: string]: { [key: string]: string };
};

// 辞書のインポート関数の型を定義
type DictionaryImportFunction = () => Promise<Dictionary>;

const dictionaries: Record<string, DictionaryImportFunction> = {
  ja: () => import('@/dictionaries/ja.json').then((module) => module.default),
  en: () => import('@/dictionaries/en.json').then((module) => module.default),
};

// 文字列("ja","en")を引数に取り、対応するjsonをインポートして返す
export const getDictionaries = async (locale: string) => dictionaries[locale]();

3. page.tsxにgetDictionariesを追加

/ja/helloのような形でアクセスしたときにjsonで定義したものを表示するためにpage.tsxにgetDictionariesを追加する。
jaの部分は動的に変わるためDynamic Routesを使う。

[lang]/hello/page.tsx
import Link from 'next/link';

import { getDictionaries } from '@/dictionaries';

export default async function Page({ params }: { params: { lang: string } }) {
  const dict = await getDictionaries(params.lang);

  return (
    <main className='flex min-h-screen flex-col items-center'>
      <div className='flex gap-8'>
        <Link href='/ja/hello'>日本語</Link>
        <Link href='/en/hello'>英語</Link>
      </div>
      <div className='flex flex-col items-center justify-center'>
        {/* dict.hello.xxxxでjsonに定義した値を表示 */}
        <h1>{dict.hello.title}</h1>
        <p>{dict.hello.text}</p>
      </div>
    </main>
  );
}

4. 言語が指定されていない場合の考慮

今のままだとhttp://localhost:3000/helloのように言語が指定されていない場合にエラーとなってしまうためmiddlewareを使ってリダイレクト処理を入れる。

middlewareの定義はプロジェクトのルート(pagesやappと同じ階層か、srcフォルダがあればsrcの中)にmiddleware.ts(または.js)で行う。

middlewareで定義するリダイレクト処理は以下の通り。

middleware.ts
import { NextResponse } from 'next/server';
import { NextRequest } from 'next/server';

const locales = ['ja', 'en'];

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );

  if (pathnameIsMissingLocale) {
    return NextResponse.redirect(new URL(`/ja/${pathname}`, request.url));
  }
}

export const config = {
  matcher: ['/((?!_next).*)'],
};

最後に

かなり簡単にi18n対応できそうですが、dictionaries/index.ts内でimport 'server-only';としてあり、これだとクライアントコンポーネントで使えないためこのまま使うのは難しそう。

この記事のコードは以下にあります。
https://github.com/hiropi1224/next-i18n

Discussion