Next.js App Routerでi18nを極める:Next-intlによる設定からType-safeまで

Development tutorial - IT technology blog
Development tutorial - IT technology blog

プロジェクト開始時の多言語対応は、もう「悩みの種」ではありません

Pages Routerでi18nを扱ったことがある方なら、SEOエラーを防ぐための手動設定や、言語切り替え時のページリロードを回避するのに苦労した記憶があるでしょう。App Routerの登場により、状況は一変しました。新しいアプローチでは、ルーティングやServer Componentsについてこれまでとは異なる考え方が求められます。

現在、数あるライブラリの中でも、next-intlは最も有力な候補として注目されています。軽量(gzippedで約10kb)であり、Server ComponentsとClient Componentsの両方を強力にサポートしています。さらに重要なのは、JSONファイルが散乱することなく、翻訳を体系的に管理できる点です。

5分で完了するクイックセットアップ

まず、プロジェクトにライブラリを追加します:

npm install next-intl

1. 標準的なディレクトリ構造

i18nをスムーズに動作させるために、すべてのルートをダイナミックセグメントである[locale]の中に配置する必要があります。これはNext.jsがURLから直接言語を認識するための標準的な手法です。理想的なディレクトリ構造は以下の通りです:

├── messages (翻訳ファイルを格納)
│   ├── en.json
│   └── ja.json
├── src
│   ├── i18n.ts (設定ローダー)
│   ├── middleware.ts (ルーティングフィルター)
│   └── app
│       └── [locale]
│           ├── layout.tsx
│           └── page.tsx
├── next.config.mjs

2. アプリケーションに設定を認識させる

messages/ja.jsonファイルを作成しましょう。ここがアプリケーションの核となる翻訳データの保存場所です:

{
  "Index": {
    "title": "itfromzeroへようこそ!",
    "description": "ゼロからのプログラミング学習。"
  }
}

次に、src/i18n.tsを設定します。このファイルは、ユーザーがアクセスしている言語に対応するデータをロードするための架け橋となります:

import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';

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

export default getRequestConfig(async ({locale}) => {
  if (!locales.includes(locale as any)) notFound();

  return {
    messages: (await import(`../messages/${locale}.json`)).default
  };
});

src/middleware.tsも忘れずに設定しましょう。これにより、ブラウザの言語設定やCookieを自動的に認識し、ユーザーを正しいURL(例:/ja/en)にリダイレクトできます:

import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  locales: ['en', 'ja'],
  defaultLocale: 'ja'
});

export const config = {
  // システムファイルとAPIをスキップ
  matcher: ['/', '/(ja|en)/:path*']
};

コアコンポーネントの詳細

Middleware:スマートな「ゲートキーパー」

ユーザーがyourdomain.comにアクセスすると、Middlewareが即座にaccept-languageヘッダーをチェックします。日本語のブラウザを使用している場合、自動的に/jaへリダイレクトされます。ヒント:常にlocalePrefix: 'always'を設定することをお勧めします。これによりURLが常に明確になり、Googleのボットが各言語バージョンを個別にインデックスできるため、SEOに非常に有利になります。

Server Componentsの威力

next-intlの最大のメリットは、Server Components上で直接動作する点です。単にあいさつ文を表示するためだけに、不要な'use client'を追加する必要はありません。これにより、ブラウザに送信されるJavaScriptの量を削減し、ページの読み込み速度を大幅に向上させることができます。

// src/app/[locale]/page.tsx
import {useTranslations} from 'next-intl';

export default function Index() {
  const t = useTranslations('Index');
  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </div>
  );
}

実務での経験:JSONファイルが数千行に達すると、管理が非常に困難になります。私はよくJSON Formatterを使用して構文を素早くチェックしています。カンマが一つ足りないだけで、アプリ全体がクラッシュする可能性があるからです。

Type-safeへのアップグレード:タイポを未然に防ぐ

キーの入力ミスは非常によくあるエラーです。例えば、JSONではtitleなのに、コード内でtitelと入力してしまうようなケースです。画面に生のキーが表示されるのを防ぐために、TypeScriptを活用してコーディング時にエラーを検知しましょう。

以下の内容でglobal.d.tsファイルを作成するだけです:

// global.d.ts
type Messages = typeof import('./messages/ja.json');
declare interface IntlMessages extends Messages {}

これで、VS Codeが各キーを自動的に補完(Intellisense)してくれるようになります。入力を間違えると、IDEが即座にエラーを表示します。これは大規模な実務プロジェクトにおいて必須の標準設定です。

スムーズな言語切り替え(Language Switcher)の実装

言語の切り替えに<a>タグを使用しないでください。ページがリロードされ、ユーザー体験が途切れてしまうからです。代わりに、next-intlで最適化されたナビゲーション関数を活用しましょう。

再利用可能なsrc/navigation.tsファイルを作成します:

import {createSharedPathnamesNavigation} from 'next-intl/navigation';

export const locales = ['en', 'ja'] as const;
export const {Link, redirect, usePathname, useRouter} = createSharedPathnamesNavigation({locales});

その後、言語選択コンポーネントは以下のようにシンプルになります:

'use client';
import {usePathname, useRouter} from '@/navigation';

export default function LocaleSwitcher() {
  const pathname = usePathname();
  const router = useRouter();

  const changeLocale = (nextLocale: string) => {
    // 現在のルートを維持したまま言語を切り替える
    router.replace(pathname, {locale: nextLocale});
  };

  return (
    <select onChange={(e) => changeLocale(e.target.value)}>
      <option value="ja">日本語</option>
      <option value="en">English</option>
    </select>
  );
}

実装における重要な注意点

  • SEOメタデータ:generateMetadataを使用してメタタグも翻訳しましょう。多言語サイトでどの言語のページタイトルも同じだと、Googleからの評価が下がってしまいます。
  • 単数形・複数形の処理:コード内でif-elseを使用しないでください。next-intlのICUフォーマットを活用しましょう:"items": "{count, plural, =0 {空} one {1項目} other {#項目}}"
  • 数値と日付のフォーマット:.toLocaleString()の代わりに、常にformat.dateTime関数を使用してください。これにより、タイムゾーンの違いによるServerとClient間のHydrationMismatchエラーを防ぐことができます。

プロジェクトの初日から i18n を導入することで、将来的な工数を大幅に削減できます。初期設定には多少の手間がかかりますが、そのプロフェッショナルな仕上がりとグローバルなユーザーへのアプローチは、それに見合う十分な価値があります。

Share: