mcpField
N
next-intl-i18n

next-intl-i18n

github.com/canatufkansu開発57 回インストール
リポジトリ

next-intl-i18n について

next-intl-i18nは、ローカライズされたルートと翻訳管理が必要なNext.js開発者向けの国際化設定例です。リクエスト設定、メッセージファイル、サーバー・クライアントコンポーネントでの翻訳、静的生成、ローカライズされたメタデータを扱います。

主な機能

  1. 01ロケールとデフォルトロケールの設定
  2. 02ロケール対応ルーティングミドルウェア
  3. 03名前空間付きメッセージファイルの整理
  4. 04補間と複数形の書式に対応
  5. 05言語切り替え機能の実装
  6. 06静的ページとローカライズ済みメタデータの生成

活用シーン

  1. 01Next.jsアプリにローカライズ済みルートを設定
  2. 02サーバー・クライアントコンポーネントで翻訳を表示
  3. 03パスを維持する言語切り替えの追加
  4. 04ローカライズ済みページとメタデータの生成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/canatufkansu/claude-skills --skill next-intl-i18n

インストール後、Claude Code で/next-intl-i18nを入力して読み込まれたか確認します。

詳細

next-intl の i18n

設定

// i18n.config.ts
export const locales = ['pt-PT', 'en', 'tr', 'es', 'fr', 'de'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'pt-PT';
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { locales, type Locale } from '@/i18n.config';

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;
  
  if (!locale || !locales.includes(locale as Locale)) {
    locale = 'pt-PT';
  }

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

ミドルウェア

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from '@/i18n.config';

export default createMiddleware({
  locales,
  defaultLocale,
  localePrefix: 'always',
});

export const config = {
  matcher: ['/', '/(pt-PT|en|tr|es|fr|de)/:path*'],
};

メッセージファイルの構造

messages/
├── pt-PT.json
├── en.json
├── tr.json
├── es.json
├── fr.json
└── de.json
// messages/en.json
{
  "nav": {
    "home": "Home",
    "about": "About",
    "services": "Services",
    "book": "Book a Session"
  },
  "hero": {
    "title": "Pilates & Yoga for strength and calm",
    "subtitle": "Transform your body and mind with personalized coaching",
    "cta": "Book Now"
  },
  "common": {
    "learnMore": "Learn More",
    "readMore": "Read More"
  },
  "form": {
    "name": "Name",
    "email": "Email",
    "message": "Message",
    "submit": "Submit",
    "errors": {
      "required": "This field is required",
      "email": "Please enter a valid email"
    }
  }
}

サーバーコンポーネント

// app/[locale]/page.tsx
import { getTranslations, setRequestLocale } from 'next-intl/server';

type Props = {
  params: Promise<{ locale: string }>;
};

export default async function HomePage({ params }: Props) {
  const { locale } = await params;
  setRequestLocale(locale); // Required for static rendering
  
  const t = await getTranslations('hero');
  
  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('subtitle')}</p>
      <button>{t('cta')}</button>
    </div>
  );
}

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

'use client';

import { useTranslations } from 'next-intl';

export function ContactForm() {
  const t = useTranslations('form');
  
  return (
    <form>
      <label>{t('name')}</label>
      <input placeholder={t('name')} />
      <button type="submit">{t('submit')}</button>
    </form>
  );
}

補間と複数形

// messages/en.json
{
  "greeting": "Hello, {name}!",
  "items": "You have {count, plural, =0 {no items} =1 {one item} other {# items}}",
  "date": "Last updated: {date, date, medium}"
}
const t = useTranslations();
t('greeting', { name: 'Maria' }); // "Hello, Maria!"
t('items', { count: 5 }); // "You have 5 items"

言語切り替え

'use client';

import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next/navigation';
import { locales, type Locale } from '@/i18n.config';

const labels: Record<Locale, string> = {
  'pt-PT': 'PT',
  'en': 'EN',
  'tr': 'TR',
  'es': 'ES',
  'fr': 'FR',
  'de': 'DE',
};

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

  const switchLocale = (newLocale: Locale) => {
    // Replace current locale in path
    const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
    router.push(newPath);
  };

  return (
    <select
      value={locale}
      onChange={(e) => switchLocale(e.target.value as Locale)}
    >
      {locales.map((loc) => (
        <option key={loc} value={loc}>
          {labels[loc]}
        </option>
      ))}
    </select>
  );
}

静的生成

// app/[locale]/layout.tsx
import { locales } from '@/i18n.config';

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

ローカライズされたメタデータ

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

type Props = {
  params: Promise<{ locale: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'meta' });

  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      canonical: `/${locale}`,
      languages: {
        'pt-PT': '/pt-PT',
        'en': '/en',
        'tr': '/tr',
        'es': '/es',
        'fr': '/fr',
        'de': '/de',
      },
    },
  };
}

ベストプラクティス

  1. 静的レンダリングのため、サーバーコンポーネントの冒頭で必ず setRequestLocale() を呼び出します
  2. 機能ごと(nav、hero、form、footer)に翻訳を名前空間で分けます
  3. すべてのロケールファイルでキーを統一します
  4. 動的な値には補間を使用し、文字列を連結しないでください
  5. ロケールを切り替える際はパスを保持します

コメント