今日、CursorやClaude Codeに「Next.jsアプリにi18nを追加して」と頼めば、十中八九、すべてのレイアウトにmiddleware.ts、setRequestLocaleが入り、useTranslationsがasyncページから呼び出されるコードが生成されます——これはNext.js 15では正解だった構成であり、Next.js 16から見ると一世代前のものです。globalize.nowはAI駆動のローカライゼーション基盤なので、私たちが注視しているのはまさにこの層です。ルーティングの配線は変わりましたが、それが読み込むカタログ自体は変わっていません。以下では、何が変わったのか、生成されたコードがどの時代のものかを見分ける方法、そして書き直しではなく午後一つで終わる3つの修正について解説します。

これらはいずれも破壊的変更ではありません。だからこそ、見落としやすいのです。

Next.js 16でi18n周りは何が変わったのか

ロケールをネゴシエートするファイルの名前が変わりました。Next.jsの公式ドキュメントでは、middlewareというファイル規約は非推奨とされ、proxyへと改名されたことがv16.0.0で明記されています。その理由は用語の問題です——「middleware」という言葉がExpressのミドルウェアと混同され続けたため、実際にはネットワーク境界を表す機能であることを反映して改名されました。

v16のこの改名には、2つの挙動上の注意点が伴います。まず、ProxyはデフォルトでランタイムとしてはNode.jsが使われ、ファイル単位のruntime設定オプションはそこではもう使えません——設定しようとするとNext.jsがエラーを投げます。さらに、Proxyは静的エクスポートでもサポートされていません。もしロケールのネゴシエーションだけがエクスポートしない理由になっているなら、これは重要なポイントです。

ロケールルーティングに関して言えば、next-intlのセットアップガイドでは現在ファイル名をsrc/proxy.tsとして示しており、Next.js 16になるまではmiddleware.tsと呼ばれていたことも明記されています。ファイル内のインポート自体は変わっていません。

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

export default createMiddleware(routing);

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

ここに非対称性があるので注意してください。よくつまずくポイントです。ファイル名はproxy.tsになりましたが、インポートパスは依然としてnext-intl/middlewareのままです。インポート名まで変更してしまうのは、ありがちな過剰修正です。

AIが生成するi18nコードが、なぜ一世代前のものになってしまうのか

コーディングエージェントは、最も多く出現するパターンを予測するからです。そして最も多く出現するパターンとは、長年かけて蓄積されたパターンにほかなりません。App Routerでのロケールルーティングについて2026年後半より前に書かれたブログ記事、Stack Overflowの回答、GitHubのサンプルコードは、どれもmiddleware.tsを使っています。モデルがこの膨大なコーパスと、登場してからまだ数週間しか経っていないv16のドキュメントを天秤にかければ、何の警告もなく、自信満々に古い形式を選んでしまうのです。

これは以前にも取り上げた問題——Cursorがi18n設定後もハードコードされた文字列を追加し続ける——と同じ仕組みです。エージェントは統計的に「普通」なコードベースを再現するのであって、あなたのコードベースを再現するわけではありません。また、指示ファイルだけではGitHub Copilotの問題を部分的にしか解決できないのもこのためです。チャット画面が読み込むルールが、インラインの補完機能にも必ず読み込まれるとは限らないのです。

実際に起きる影響は限定的ですが、確かに存在します。生成されたセットアップは一応動くので、何も派手には壊れません。ところが、あるバグに突き当たって検索してみると、今出てくる回答はどれも、あなたの手元にはないファイルの話をしているのです。

生成されたセットアップがどの時代のものか、どう見分ければいいですか?

4回のgrepで済みます。プロジェクトのルートで実行すれば、1分ほどで判別できます。

# 1. Pre-16 locale negotiation file
ls middleware.ts src/middleware.ts 2>/dev/null

# 2. Legacy static-rendering API
grep -rn "setRequestLocale" app src 2>/dev/null

# 3. Hooks called inside async components
grep -rn -B3 "useTranslations" app | grep -n "async function"

# 4. Which next-intl era the config reads from
grep -rn "root-params\|await params" src/i18n/request.ts 2>/dev/null

1と2でヒットするなら、v16以前のスキャフォールディングです。3でヒットする場合は、そのコンポーネントを最初にレンダーするリクエストが来た瞬間に発生する、正真正銘のランタイムエラーです。4でヒットしない場合は、リクエスト設定が古い方式でロケールを読み取っていることを意味します。

middleware.tsをproxy.tsに改名する必要はありますか?

急いでやる必要はありませんし、手作業でやるべきでもありません。Next.jsには、ファイル名とエクスポートされる関数名の両方を改名してくれるcodemodが用意されています。

npx @next/codemod@canary middleware-to-proxy .

この改名は非推奨化であって削除ではないので、既存のmiddleware.tsはそのまま動作し続けます。それでも実行しておく理由はメンテナンスコストです。ファイル名を最新のドキュメントに合わせておけば、今後の検索結果はすべてそのままあなたのリポジトリに当てはまるようになります。放置しておくと、デバッグのたびに小さな税金を払い続けることになります、いつまでも。

next-intlでsetRequestLocaleは非推奨になったのですか?

「レガシー」というマークが付けられていますが、これは「非推奨」よりも穏やかな表現であり、正確に理解しておく価値があります。next-intlのドキュメントでは、setRequestLocaleはnext/root-paramsが導入されるまで存在していたAPIだと説明されており、後方互換性のためにサポートは継続するものの、next/root-paramsの使用が推奨されています。

新しい方式では、マッチしたロケールをすべてのレイアウトやページに手動で受け渡していくのではなく、リクエスト設定の中で読み取るようになります。

// src/i18n/request.ts
import * as rootParams from 'next/root-params';
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({locale}) => {
  if (!locale) {
    const paramValue = await rootParams.locale();
    if (hasLocale(routing.locales, paramValue)) {
      locale = paramValue;
    } else {
      notFound();
    }
  }

  return {locale};
});

next/root-paramsはNext.js 16.3以降であればデフォルトで利用可能です。それより前のバージョンではexperimental.rootParamsを通じて有効化する必要があります。この手順に従えば静的レンダリングも無料でついてきます。ただし、[locale]セグメントに対してgenerateStaticParamsをエクスポートしている限りにおいてです。

以前の方式では、静的にレンダリングしたいすべてのページとレイアウトで、他のnext-intl呼び出しより前にsetRequestLocaleを必ず呼び出す必要がありました。これはNext.jsがレイアウトとページを独立してレンダリングするためです。これは、AIエージェントが5つ目のファイルを書く頃には忘れてしまうようなルールです。この要件自体をなくしてしまえば、このクラスのバグごと消えてなくなります。

非同期のServer Componentの中でuseTranslationsがクラッシュするのはなぜですか?

フックはasyncなコンポーネントの中からは呼び出せず、useTranslationsはフックだからです。これはReact Server Componentsの制約であり、next-intl固有の癖ではありません。next-intlの答えは、await可能な関数群を並行して用意することです。

// Async component — await the server API
import {getTranslations} from 'next-intl/server';

export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getTranslations('ProfilePage');
  return <h1>{t('title', {username: user.name})}</h1>;
}
// Non-async component — the hook is correct here
import {useTranslations} from 'next-intl';

export default function UserDetails({user}) {
  const t = useTranslations('UserProfile');
  return <h2>{t('title')}</h2>;
}

getFormatter、getNow、getTimeZone、getMessages、getLocaleも同じパターンに従います。2番目の例はじっくり考える価値があります。インタラクティブな機能を持たない非同期でないコンポーネントは共有コンポーネントであり、next-intlはそれがサーバー側でレンダリングされるかクライアント側でレンダリングされるかに応じて、適切な実装を解決してくれます。つまり、Server Componentの中でuseTranslationsを呼ぶこと自体は間違いではありません——間違いは、それをasyncなコンポーネントから呼び出すことです。

NextIntlClientProviderのコンテキストエラーが出るのはなぜですか?

next-intlのトラブルシューティングでは2つの原因が挙げられており、それぞれ逆の対処法が必要です。1つは、そのコンポーネントが実際にクライアント側で実行されているのに、上位にProviderがない場合です。この場合はProviderでラップして、必要なメッセージを渡してください。もう1つは、サーバー側でレンダリングされるはずだったのに、いつの間かクライアントモジュールのグラフに紛れ込んでしまった場合です。この場合は、Server Componentの内部でインポートするのではなく、childrenとして渡してください。

AIが生成したアプリでよく遭遇するのは2番目のケースです。エージェントはインタラクティブ性を実現するために気前よく'use client'を適用しがちで、このディレクティブはインポートグラフを通じて伝染していきます。推奨されるパターンは、サーバー側で翻訳を済ませ、完成した文字列だけを境界の向こうへ渡すことです。

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

export default function FAQEntry() {
  const t = useTranslations('FAQEntry');
  return <Expandable title={t('title')}>{t('description')}</Expandable>;
}

もし本当にクライアント側でメッセージが必要なコンポーネントがある場合は、すべてのメッセージをブラウザに送るのではなく、その部分木だけを囲むProviderを用意することもできます——ルートのProviderにmessages={null}を指定すれば、メッセージは一切渡されません。

これらの変更で、変わらないものは何ですか?

ロケールファイルです。上に挙げた修正はすべてルーティングとレンダリングの配線に関するものであり、アプリが読み込むJSON自体には何の影響もありません。ここが持ち帰るべきポイントです。配線は年に一度の頻度で変わる、午後一つで終わる作業ですが、カタログの方は新しいUIが出荷されるたびに毎週劣化していくものだからです。

これこそが私たちが前提としている役割分担です。next-intlやその仲間たちは実行時に翻訳を配信する役割を担っており、私たちはそこと競合するものではありません。もしまだその選定に迷っているなら、next-intl対react-i18next対Linguiの比較記事でトレードオフを整理しています。globalize.nowはその一段上の層に位置し、これらのランタイムが読み込むキーとロケールファイルを生成します。だからこそ、Next.js統合は、ロケールネゴシエーションがmiddleware.tsにあるかproxy.tsにあるかを気にしません。

変換作業は一度限りで、アプリ側で完結します。リポジトリを接続すると、globalize.nowがコードベースを一度だけ変換し、カタログを添えたプルリクエストを開きます。その後は、新しいカタログ単位が現れるたびにプッシュジョブが翻訳を行います——翻訳ファイルが同期からずれ続けてしまう理由で説明した失敗モードを回避するためのもので、3つ目のロケールを追加する前に、この隙間を埋めておく価値があります。

すでに手書きのnext-intlセットアップがあるプロジェクトについては、私たちが何に手を加え、何に手を加えないかを正確に解説した記事があります。まだセットアップがない場合は、Cursorのウォークスルーが最短ルートです。ただし、そこで生成されるファイル名は古い規約に基づいているので、その後codemodを実行してください。

どこから始めるか

4回のgrepを実行してください。最初の2つでヒットしたら、codemodを実行し、リクエスト設定をnext/root-paramsに移行すれば、最新の状態になります。次にロケールファイルを見てください。来月になっても、まだずれ続けているのはそこだからです。AIで作ったアプリを運用していて、カタログを「メンテナンスする」のではなく「任せてしまいたい」なら、vibe coders向けページが概要で、料金は別ページにまとまっています。

globalize.nowは、ハードコードされたアプリ内テキストを翻訳可能なロケールファイルに変換し、リリースのたびに自動で最新の状態を保ちます。

globalize.nowを無料で試す