Next.jsのi18n対応 - App routerでのnext-intlの導入から実装まで

# Next.jsのi18n対応 - App routerでのnext-intlの導入から実装まで

## はじめに

最近Webアプリケーションの国際化のために、色々調べてnext-intlを使用してみた。
本記事では、next-intlを使用したi18n実装の手順と、実装過程で遭遇した個人的なハマりポイントについて記載する。

## 想定読者・開発者

- Next.jsでのi18n対応を検討している
- App Routerを使用している
- 多言語対応のWebアプリケーション開発

## 開発環境

- Next.js: 14.2.x
- next-intl: 3.26.x
- TypeScript: 5.x

## i18n (Internationalization)について

i18nは「Internationalization（国際化）」の略語で、単語の頭文字「i」と末尾の「n」の間に18文字があることに由来する。Webアプリケーションを複数の言語や文化に対応させるための設計および実装プロセスを指す。

主な対応項目：

- 言語翻訳：UIテキスト、メッセージ、エラーメッセージの多言語対応
- 地域設定：日付、時間、数値、通貨の地域別表示形式
- 文字エンコーディング：Unicode（UTF-8など）による多言語サポート
- RTL言語対応：アラビア語やヘブライ語などの右書き言語のサポート

### i18n対応の必要性評価

#### ビジネス面での利点
- グローバル市場へのリーチ拡大
- ユーザー基盤の拡大
- 地域別法規制（GDPRなど）への対応

#### 技術面での利点
- コンテンツの一元管理による保守性向上
- 言語切り替えの一貫性確保
- SEO対策の効率化

### next-intlの個人的な選定理由

- Next.js App Routerとの高い親和性
- 充実したTypeScriptサポート
- SSRとの優れた統合性
- 軽量な実装

代替候補：
- react-intl
- i18next

## i18nルーティング方式の選択

next-intlの実装を開始する前に、プロジェクトに適したルーティング方式を選択する必要がある。

### i18nルーティングありの場合
- URLパスに言語コードを含める（`/en/about`, `/ja/about`）
- ドメインベースの言語切り替えが可能
- SEO最適化されたURL構造
- 適用例：
  - 多言語コンテンツを提供する公開Webサイト
  - 言語別URLが必要なSEO重視のサイト
  - ユーザーによる明示的な言語選択が必要なケース

### i18nルーティングなしの場合
- URLに言語コードを含めない
- シンプルなURL構造を維持
- システム設定に基づく自動言語切り替え
- 適用例：
  - 単一言語のアプリケーション
  - シンプルなURL構造を重視するケース
  - ユーザー設定による自動言語切り替えを行うケース

## i18nルーティングの実装手順

[Next.js App Router with i18n routing](https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing)

### プロジェクトのディレクトリ構造

```
├── messages/
│   ├── en.json
│   └── ja.json
├── next.config.mjs
└── src/
    ├── i18n/
    │   ├── routing.ts
    │   └── request.ts
    ├── middleware.ts
    └── app/
        └── [locale]/
            ├── layout.tsx
            └── page.tsx
```

### 1. パッケージのインストール

```bash
npm install next-intl@latest
```

### 2. メッセージファイルの作成

```json
// messages/en.json
{
  "HomePage": {
    "title": "Hello world!",
    "about": "Go to the about page"
  }
}
```

### 3. next.config.mjsの設定

```javascript
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin();

/** @type {import('next').NextConfig} */
const config = {};

export default withNextIntl(config);
```

### 4. ルーティング設定

```typescript
// src/i18n/routing.ts
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);
```

### 5. ミドルウェアの設定

```typescript
// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
 
export default createMiddleware(routing);
 
export const config = {
  matcher: ['/', '/(de|en)/:path*']
};
```

### 6. リクエスト設定

```typescript
// src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  if (!locale || !routing.locales.includes(locale as "en" | "ja")) {
    locale = routing.defaultLocale;
  }

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

### 7. レイアウト設定

```typescript
// src/app/[locale]/layout.tsx
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};
}) {
  if (!routing.locales.includes(locale as any)) {
    notFound();
  }
 
  const messages = await getMessages();
 
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}
```

## useTranslationsの使用方法

```typescript
import {useTranslations} from 'next-intl';
import {Link} from '@/i18n/routing';
 
export default function HomePage() {
  const t = useTranslations('HomePage');
  return (
    <div>
      <h1>{t('title')}</h1>
      <Link href="/about">{t('about')}</Link>
    </div>
  );
}
```

## 自分が少しハマった点

以下に他のi18nを使用したことのない自分がnext-intlを使用する際にハマった点を記す。
このような時は公式ドキュメントをやはり読むのが良い。
[next-intl Official Documentation](https://next-intl.dev/)

### 1. Next.jsのバージョンとルーティング方式

- Pages RouterとApp Routerで実装方法が異なる
- バージョンによって推奨実装が変化
- 情報が混在し、適切な実装の見極めに時間がかかった

#### 確認内容
1. プロジェクト環境の確認
   - Next.jsのバージョン確認
   - ルーティング方式の確認
   - next-intlのバージョン確認

2. 実装パターンの選択
   - App Router：`src/i18n/`ディレクトリ構造を採用
   - Pages Router：`i18n.ts`単一ファイルでの実装

### 2. リンクとナビゲーションの実装

- `next/link`でi18n対応ルーティングが機能しない
- `next/navigation`の`useRouter`で言語設定の考慮が困難

#### 確認した内容
1. Linkコンポーネントの移行
   - `next/link`から`@/i18n/routing`のLinkへの変更
   - 相対パスでhref指定

2. useRouterの移行
   - `next/navigation`から`@/i18n/routing`のuseRouterへの変更
   - localeの自動処理の有効化

## まとめ

初めてのi18n実装でnext-intlを使用した際に、以下の点が重要であると感じた：

1. プロジェクトの要件に応じた適切なルーティング方式の選択
2. Next.jsとnext-intlのバージョンの確認と適切な実装パターンの採用
3. Link componentとuseRouterの適切な実装による多言語対応の実現

## 参考文献

- [next-intl Official Documentation](https://next-intl.dev/)
- [Next.js App Router with i18n routing](https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing)
- [Next.jsのApp Routerでi18n（国際化）対応](https://qiita.com/GleapPost/items/623bc129fb4d0396b7f9)

この記事が、Next.jsのi18n実装の参考になれば幸いである。