Next.js アプリを翻訳したのに、ブラウザのタブ、404 ページ、フォームエラー、リンクプレビューがまだ英語のまま。その原因はほぼ決まっています。これらの文字列はコンポーネントの外で生成されるため、useTranslations の呼び出しが一度も及んでいないのです。globalize.now は AI 搭載のローカライズ基盤ですが、AI エージェントが App Router プロジェクトに i18n を追加したとき、この層が中途半端なまま残るケースをよく目にします。画面に見えるページは変換されても、それ以外の領域は手つかずなのです。
以下では、それぞれの文字列がどこに潜んでいるのか、そして対応する next-intl の呼び出しを紹介します。すべての例は、2026-10-28 時点の next-intl 公式ドキュメントに沿っています。
App Router アプリで、未翻訳の文字列はどこに潜んでいる?
テキストを描画する React コンポーネントではない、6 つの場所に潜んでいます。コンポーネントを順に処理して i18n を追加するコーディングエージェントは、画面上の UI を変換する一方で、これらを素通りしてしまいます。
generateMetadata内のページタイトルと description。- Server Actions のバリデーションエラーや結果メッセージ。
not-found.tsx、つまり 404 ページ。error.tsx、つまり実行時エラーページ。opengraph-image.tsx、manifest.ts、sitemap.tsなどの生成ファイル。- テキストを返す Route Handler。
共通点は、いずれも [locale] のレイアウトツリーの中で通常の形でレンダリングされないため、ロケールを明示的に渡すか、別の方法で読み取る必要があることです。Next.js 16 のルーティングの記事ではルーティングの設定を扱いました。この記事では、その設定が届かない部分を扱います。
ページタイトルと description はどう翻訳する?
generateMetadata の中で next-intl/server の getTranslations を呼び出します。この関数は page でも layout でもエクスポートでき、非同期なので、await できる API を使うのが正解です。
// app/[locale]/page.tsx
import {getTranslations} from 'next-intl/server';
export async function generateMetadata() {
const t = await getTranslations('Metadata');
return {title: t('title'), description: t('description')};
}
落とし穴は静的な書き方です。エージェントは短く書けるという理由で export const metadata = {title: 'Dashboard'} を書きがちですが、定数から翻訳関数は呼び出せません。関数形式に書き換え、同じキーをすべてのロケールのカタログに追加してください。これは検索結果に表示されるテキストでもあり、メタデータを翻訳せずにアプリだけ翻訳しても、検索順位にはほとんど効果がないのはそのためです。
Server Action とバリデーションメッセージはどう翻訳する?
アクションにロケールを渡し、名前空間とそのロケールの両方を指定して getTranslations を呼び出します。Server Action はページの言語を知らないため、next-intl のドキュメントでは、たとえば引数として明示的に渡すようにとされています。
'use server';
import {getTranslations} from 'next-intl/server';
export async function subscribe(locale: string, formData: FormData) {
const t = await getTranslations({locale, namespace: 'Subscribe'});
// ...validate, then return t('success') or t('invalidEmail')
}
同じドキュメントからもう一点注意があります。アクションのメッセージが表示されている最中に、訪問者が言語を切り替えられる場合を考えてください。URL に [locale] セグメントがあれば、ルートが変わるので自動的に処理されます。ない場合は、たとえばコンポーネントに key={locale} を与えて state をリセットするなど、メッセージを自分でクリアする必要があるかもしれません。
Zod でのフォームバリデーションには、エラーマップを使う方法がドキュメントで紹介されています。Zod は safeParse の呼び出しごとにエラーマップを受け取り、そのマップの中で getTranslations を呼び出せます。これを 1 つの非同期ヘルパーにまとめ、その戻り値を各パースの error オプションに渡してください。そうすればスキーマに書かれるのは英文ではなくルールだけになり、AI が生成したフォームがラトビア語のページに "Invalid email" を出してしまうこともなくなります。
404 ページとエラーページはどう翻訳する?
app/[locale]/ の中に not-found.tsx を置き、実際に呼び出されるようキャッチオールを追加します。Next.js はセグメントが notFound() を呼び出したときにこのファイルをレンダリングしますが、未定義の URL では自動的には呼び出されません。
// app/[locale]/[...rest]/page.tsx
import {notFound} from 'next/navigation';
export default function CatchAllPage() {
notFound();
}
next-intl のミドルウェアにマッチしないリクエストにはロケールがまったく含まれないため、[locale] の 404 は適用されません。ドキュメントの答えは app/global-not-found.tsx です。これは Next.js のドキュメントでは実験的機能とされており、独自の html と body をレンダリングします。未定義の [locale] の値については、通常は i18n/request.ts の中で notFound() を呼び出すようドキュメントに記載されています。
error.tsx は事情が異なります。Next.js ではこれが Client Component であることが必須で、レイアウトの配下でレンダリングされるため、少なくとも Error 名前空間を持たせた NextIntlClientProvider が必要です。カタログ全体ではなく、そのメッセージだけを渡してください。
// app/[locale]/layout.tsx (excerpt)
const messages = await getMessages();
// ...
<NextIntlClientProvider locale={locale} messages={pick(messages, 'Error')}>
これで error.tsx の中でも useTranslations('Error') が使えます。このページは global-error をカバーしないので、該当ファイルがある場合は手作業で確認してください。
シェア画像、manifest、sitemap はどうする?
いずれもレイアウトツリーの外にあるため、ロケールを明示的に渡す必要があります。opengraph-image.tsx では、params から locale を読み取り、getTranslations({locale, namespace}) を呼び出します。localePrefix をカスタマイズしている場合、Next.js は proxy(ミドルウェア)のリライトを認識しないため、生成されたルートに到達できなくなることがある、とドキュメントで警告されています。対処法は、opengraph-image で終わるパスをマッチャーの対象から除外することです。
app/manifest.ts は [locale] の外にあるため、next-intl はパスからロケールを推測できません。代表的なロケール(通常はデフォルトのもの)を選び、明示的に渡してください。sitemap.ts では alternates.languages エントリを追加し、ナビゲーションモジュールの非同期 getPathname({locale, href}) で各 URL を組み立て、ホスト名を前に付けます。next-intl はデフォルトで、各言語版を示す link レスポンスヘッダーも送信します。CMS 由来のスラッグを細かく制御したい場合は、sitemap を使う方法があります。
Route Handler はどう扱う?
ロケールは検索パラメータ、セグメント、または accept-language ヘッダーから取得し、hasLocale(routing.locales, locale) で検証して、失敗したら 400 を返します。その後 getTranslations({locale, namespace}) を呼び出してください。検証を省くと、任意の文字列がカタログの参照にそのまま届いてしまいます。
エージェントが残した英語はどうやって見つける?
プロジェクトのルートで次のコマンドを実行してください。あえて大まかな作りにしてあります。目的はスコアを出すことではなく、目で確認するためのリストを得ることです。
# 1. Static metadata that cannot be translated
grep -rn "export const metadata" app
# 2. Titles and descriptions already in generateMetadata
grep -rn "title:" app --include=page.tsx --include=layout.tsx
# 3. Server Actions with literal return messages
grep -rln "use server" app src 2>/dev/null | xargs grep -n "message:\|error:"
# 4. Zod schemas with inline messages
grep -rn "message:\s*['\"]" app src 2>/dev/null
# 5. 404 and error files, and whether a catch-all exists
ls app/\[locale\]/not-found.tsx app/\[locale\]/error.tsx "app/[locale]/[...rest]/page.tsx" 2>&1
最初の 4 つの結果にあるプレーンな英語のリテラルは、すべてカタログに入れるべきものです。エージェントがハードコード文字列を追加し続ける理由では、この問題が繰り返し起きる理由を説明しています。エージェントは目に見えるパターンに従い、その目に見えるパターンがコンポーネントなのです。Copilot をお使いの場合は、Copilot の instruction ファイルの制限もここで当てはまります。
これでカタログはどう変わる?
キーが増えます。そして、ずれはキーから始まります。上記のどの項目も、Metadata、Error、Subscribe やバリデーションメッセージといった新しいエントリをカタログに追加し、そのすべてについて、提供する全ロケールの翻訳が必要です。コンポーネントを一度翻訳しただけで、あとから Server Action や新しいページタイトルを追加するプロジェクトは、翻訳ファイルが同期ずれを起こす理由で説明しているプロジェクトそのものです。
next-intl は実行時に翻訳を配信する役割を担っており、私たちがそれを置き換えることはありません。globalize.now はその一段上の層に位置します。アプリでリポジトリを接続すると、コードベースが一度だけカタログに変換され、その後はプッシュジョブによって新しいカタログユニットが登場するたびに翻訳され、レビュー可能な変更として届きます。Next.js 連携にセットアップ手順があり、next-intl をすでに手作業で組み込んでいる場合は、既存の i18n 構成で私たちが触れる部分と触れない部分をご覧ください。
どこから始めるか
5 つの grep を実行し、静的な metadata エクスポートがあれば変換し、キャッチオールルートを追加して、error.tsx にプロバイダーを設定しましょう。そのうえで、新しい名前空間がすべてのロケールのカタログに入っているかを確認してください。AI で作ったアプリをリリース済みで、変換と翻訳を自分で保守するのではなく任せたい場合は、バイブコーダー向けページで全体像を、開発者向けリファレンスでエージェント構成の詳細を確認でき、料金は専用のページにまとめています。
globalize.nowは、ハードコードされたアプリ内テキストを翻訳可能なロケールファイルに変換し、リリースのたびに自動で最新の状態を保ちます。
globalize.nowを無料で試す