naopoyo.com
  • Docs
  • Tags
  • Bookmarks
  • Tools
  • About
  • Docs
  • Tags
  • Bookmarks
  • Tools
  • About

目次

© naopoyo

目次

最近更新された記事

🍶

API-only Rails + GraphQL の rack-mini-profiler セットアップ

約6時間前·2026年02月11日
  • Rails
  • GraphQL
  • Next.js
🔍

Claude Code の Explore エージェントについて

4日前·2026年02月07日
  • Claude Code
🚣

Tailwind CSS v4 で max-h-[300px] を max-h-75 に自動修正するための設定

5日前·2026年02月07日
  • Tailwind
  • ESLint
🛬

Next.js 16 + next-intl で多言語化対応するための設定と注意点

公開日約2時間前2026-02-11
履歴GitHubで見る
MarkdownRaw Content
  • Next.js
  • TypeScript

概要

Next.js 16 と next-intl(v4.8.2)で多言語化を実装する際の設定手順と、ハマりやすいポイントをまとめた。

まとめ

  • Next.js 16 で middleware.ts が proxy.ts に移行、params 等の同期アクセスも完全廃止
  • cacheComponents + Parallel Routes の動的セグメントで Suspense エラーが発生する
  • 解決策は [locale]/layout.tsx を同期にし、<html> ごと <Suspense> 内の非同期コンポーネントに委譲するパターン

Next.js 16 で変わったこと

middleware.ts → proxy.ts

middleware.ts が廃止され proxy.ts に置き換わった。proxy.ts は Node.js ランタイムのみで動作するため、Edge Runtime を前提にしていたコードは見直しが必要になる。

params の同期アクセスが完全廃止

Next.js 15 では deprecation warning だった params の同期アクセスが、16 ではエラーになる。searchParams、cookies()、headers() も同様。

// Next.js 16(await が必須)
export default async function Layout({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params;
  return <html lang={locale}>...</html>;
}

既存プロジェクトには codemod が用意されている。

npx @next/codemod@canary upgrade latest
npx @next/codemod@latest next-async-request-api .

Parallel Routes に default.tsx が必須

[locale] 配下で Parallel Routes を使っている場合、すべてのスロットに default.tsx が必要になった。

セットアップ手順

1. 翻訳ファイル

// messages/ja.json
{
  "common": {
    "signIn": "サインイン",
    "signOut": "サインアウト"
  },
  "home": {
    "title": "ようこそ",
    "description": "{name}さんのダッシュボード"
  }
}

同じ構造で messages/en.json 等を用意する。

2. next.config.ts

next.config.ts
import createNextIntlPlugin from 'next-intl/plugin'

const withNextIntl = createNextIntlPlugin()

const nextConfig = {
  // ...
}

export default withNextIntl(nextConfig)

3. リクエスト設定

i18n/request.ts
import { getRequestConfig } from 'next-intl/server'

export default getRequestConfig(async ({ requestLocale }) => {
  const locale = (await requestLocale) || 'ja'

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

4. proxy.ts でのロケール検出

proxy.ts
import { locales, defaultLocale } from './i18n/config'

export function proxy(request: Request) {
  const url = new URL(request.url)
  const pathname = url.pathname

  const pathnameLocale = locales.find(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )

  if (pathnameLocale) return

  const acceptLanguage = request.headers.get('accept-language')
  const detectedLocale = negotiateLocale(acceptLanguage) || defaultLocale

  return Response.redirect(new URL(`/${detectedLocale}${pathname}`, request.url))
}

5. レイアウト

Parallel Routes を使わない(または cacheComponents を使わない)場合は、シンプルな async layout で問題ない。

app/[locale]/layout.tsx
import { setRequestLocale } from 'next-intl/server'
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'

export function generateStaticParams() {
  return [{ locale: 'ja' }, { locale: 'en' }]
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<{ locale: string }>
}) {
  const { locale } = await params
  setRequestLocale(locale)
  const messages = await getMessages()

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  )
}

Parallel Routes + cacheComponents を使う場合は、このコードではエラーになる。 次のセクションで解決策を説明する。

6. コンポーネントでの使用

Server Component でも Client Component でも useTranslations が使える。

// Server Component
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';

export default async function Home({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params;
  setRequestLocale(locale);
  const t = useTranslations('home');

  return <h1>{t('title')}</h1>;
}
// Client Component
'use client';
import { useTranslations } from 'next-intl';

export default function SignOutButton() {
  const t = useTranslations('common');
  return <button>{t('signOut')}</button>;
}

cacheComponents + Parallel Routes での Suspense エラー

Next.js 16 では PPR と dynamicIO が cacheComponents に統合された。この機能を有効にした状態で、Parallel Routes 内に動的セグメント([documentId] など)があると、直接アクセス(ハードナビゲーション)時に以下のエラーが出る。

Uncached data was accessed outside of `<Suspense>`

クライアントサイドナビゲーションでは発生しない。

原因

Parallel Routes には「1つのスロットが dynamic なら、同レベルの全スロットも dynamic になる」というルールがある。パラレルルート内の [documentId] のような動的セグメントがルートツリー全体を dynamic 扱いにし、[locale]/layout.tsx の await params が Suspense 外でのランタイムデータアクセスとしてエラーになる。

エラーのトレースは [locale]/layout.tsx を指すが、直接の原因は パラレルルート内の動的セグメントの存在 だ。

効かない対策

対策効かない理由
loading.tsx の追加page をラップするだけで、parent layout の await params はカバーしない
page を同期 + 子を <Suspense> で囲む問題は parent layout チェーンにある
generateStaticParams の定義パラレルルートの動的セグメントが上書きする
<html> を外に置き <body> 内だけ Suspenseawait params が layout 関数内に残る

解決策

[locale]/layout.tsx を同期コンポーネントにし、<html> タグを含む全コンテンツを <Suspense> 内の非同期コンポーネントに移動する。

// app/[locale]/layout.tsx
import { Suspense } from 'react';
import { getMessages, setRequestLocale } from 'next-intl/server';
import { NextIntlClientProvider } from 'next-intl';

export function generateStaticParams() {
  return [{ locale: 'ja' }, { locale: 'en' }];
}

async function LocaleContent({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  setRequestLocale(locale);
  const messages = await getMessages();

  return (
    <html lang={locale} suppressHydrationWarning>
      <body>
        <NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

export default function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  return (
    <Suspense fallback={null}>
      <LocaleContent params={params}>{children}</LocaleContent>
    </Suspense>
  );
}

ポイントは <html> タグごと <Suspense> の中に入れる こと。これにより await params が Suspense 境界内に収まり、<html lang> も SSR で正しく出力される。

cacheComponents を使わない場合はこの問題は発生しないが、今後デフォルト有効になる可能性があるため、setRequestLocale() を呼ぶ習慣はつけておくとよい。

Next.js 15 からの移行チェックリスト

  1. middleware.ts を proxy.ts に移行する
  2. params の同期アクセスをすべて await に変更する
  3. cookies() や headers() を使ったロケール検出を非同期に変更する
  4. Parallel Routes を使っている場合は default.tsx を追加する
  5. cacheComponents を有効にする場合は setRequestLocale() を忘れずに呼ぶ
  6. Parallel Routes + cacheComponents の場合は locale layout を Suspense パターンに変更する
  7. Edge Runtime を前提にしたミドルウェアコードを見直す(proxy.ts は Node.js ランタイムのみ)