BRINK ジャパン株式会社

ブログ一覧へ

next-intlでNext.jsアプリを多言語化(i18n)

はじめに:NextIntlとは

NextIntl は Next.js(特に App Router)向けに設計された国際化ライブラリ。Server Components や静的レンダリングと自然に連携し、多言語対応をシンプルに管理できる。

1. 主な特徴

ICU メッセージ構文

```

{count, plural,
  =0 {カートに商品はありません}
  =1 {1 商品}
  other {# 商品}}

複数形や条件分岐を標準 API で記述できる。

型安全

TypeScript と統合されており、メッセージキーの補完・型チェックが効く。

Next.js 最新機能と連携

App Router/Server Components/静的レンダリングなどと相性が良い。

日付・数値フォーマット

ロケールに合わせて日時・通貨・相対時刻を自動フォーマット。

2. 導入と初期設定

インストール

npm install next-intl
# または
yarn add next-intl

プロジェクト構成例

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

next.config.ts

import createNextIntlPlugin from 'next-intl/plugin';

export default createNextIntlPlugin('./i18n/request.ts')({});

i18n/routing.ts

import {defineRouting} from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['en', 'ja'],
  defaultLocale: 'en',
  localePrefix: 'always'
});

middleware.ts

import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

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

3. App Router と Pages Router の違い

App Router(推奨)

サーバーコンポーネント

import {getTranslations} from 'next-intl/server';

export default async function HomePage() {
  const t = await getTranslations('HomePage');
  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </main>
  );
}

#### クライアントコンポーネント

'use client';
import {useTranslations} from 'next-intl';

export default function SubmitButton() {
  const t = useTranslations('Common');
  return <button>{t('submit')}</button>;
}

Pages Router

getStaticPropsで翻訳データを渡す必要がある。新規プロジェクトでは App Router の使用が簡潔。

4. 実装例

言語切り替え UI

'use client';
import {useLocale} from 'next-intl';
import {useRouter, usePathname} from '@/i18n/navigation';

export default function LanguageSwitcher() {
  const locale = useLocale();
  const router = useRouter();
  const pathname = usePathname();

  return (
    <select
      value={locale}
      onChange={(e) => router.replace(pathname, {locale: e.target.value})}
    >
      <option value="en">English</option>
      <option value="ja">日本語</option>
    </select>
  );
}

日付・数値フォーマット

import {useFormatter} from 'next-intl';

export default function FormattingSample() {
  const f = useFormatter();
  const now = new Date();
  const price = 1499.99;

  return (
    <div>
      <p>{f.dateTime(now, 'medium')}</p>
      <p>{f.number(price, {style: 'currency', currency: 'USD'})}</p>
      <p>{f.relativeTime(now)}</p>
    </div>
  );
}

5. ベストプラクティス

メッセージの分割読み込み

const messages = {
  ...(await import(`../messages/${locale}/common.json`)),
  ...(await import(`../messages/${locale}/pages/${page}.json`))
};

静的レンダリング

export function generateStaticParams() {
  return routing.locales.map((locale) => ({locale}));
}

SEO メタデータ

export async function generateMetadata() {
  const t = await getTranslations('Metadata');
  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      languages: {
        en: 'https://example.com/en',
        ja: 'https://example.com/ja'
      }
    }
  };
}

6. よくあるミスとその対応

  • Unable to find next-intl locale エラーは、config.matcher に _vercel や静的ファイルの除外がなくミドルウェアが実行されないことが原因。matcher: ['/((?!api|_next|_vercel|.*\\..*).*)] などの推奨パターンを使う。
  • CDN 経由で X-NEXT-INTL-LOCALE ヘッダーが消えるのは、リバースプロキシがカスタムヘッダーを転送しないため。ヘッダーを明示的にフォワードするか、ライブラリの headerName オプションで名称を変更する。
  • 日付や数値のハイドレーション不一致は、サーバーとクライアントでそれぞれ new Date() を生成するために起きる。親の Server Component で日時を生成して props で渡すか、cache() で固定値にする。
  • MISSING_MESSAGE が大量に出力されるのは、動的ロードにより翻訳キーが未定義のまま使われるため。onError を使ってログ出力を制御・フィルタする。
  • クライアント側で getTranslations() を使うとクラッシュするのは、同関数が Server 専用だから。クライアントでは useTranslations() を使う。

まとめ

  • NextIntl は App Router と組み合わせると翻訳取得とルーティングを最小構成で保てる。
  • 型補完によりメッセージキーのタイポを防止できる。
  • `getTranslations` と `useTranslations` の役割を分けておくとハイドレーションエラーを避けやすい。
  • ページ単位で翻訳ファイルを分割し、`generateStaticParams` を活用すると通信量を抑えられる。
  • `generateMetadata` で hreflang を集中管理すると SEO の漏れを防げる。
  • Server Components 側で翻訳取得を行うとクライアントバンドルをさらに小さくできる。