Next.js i18n
將 Next.js 應用程式放入多語言的結構化指引:資料夾配置應如何、配置錯誤時會發生什麼問題,以及應避免什麼。
先選擇函式庫
對於 App Router 上的新專案,預設使用 next-intl。它不需要建置外掛、巨集步驟或每個地區的實例管理;訊息是基於來源語言的純 JSON 型別。
| 函式庫 | 適用時機 | 成本 |
|---|---|---|
| next-intl (預設) | 新的 App Router 專案,任何團隊規模 | 無 — 純 JSON,無代碼生成 |
| @lingui/react | 需要編譯時提取和更小的執行時期,或希望一個 i18n 層跨 React/Vue/Astro | SWC 或 Babel 巨集外掛,需精確釘版 |
| next-i18next | 團隊已熟悉 i18next,或正在遷移 Pages Router 程式碼 | 每個請求實例,getT/useT 分開 |
Next.js 內建 i18n | 僅限 Pages Router。 絕不用於 App Router | 不適用 |
Next.js 的 i18n: { locales, defaultLocale } 配置僅適用於 Pages Router。在 App Router 中它不做任何事 — 那裡的地區路由意味著一個 [locale] 區段加上一個 proxy。不要讓代理程式為 App Router 專案「啟用」內建的 i18n。
完整的比較,包括套件大小和每個函式庫的取捨:references/library-comparison.md。
規範架構
將靜態字典放在 src/ 之外,以便打包和匯入路徑保持明確,並將所有地區相關機制保留在一個資料夾中。
messages/ # 純靜態字典 — 保持於 /src 之外
├── en.json # 原始來源;每個其他地區都鏡像它
├── de.json
└── ja.json
src/
├── i18n/
│ ├── routing.ts # defineRouting: 地區 + 預設地區
│ ├── request.ts # getRequestConfig: 解析地區,載入訊息
│ └── navigation.ts # createNavigation: 感知地區的 Link/redirect
├── proxy.ts # 地區偵測 + 重定向 (Next 16;以前是 middleware.ts)
└── app/
├── [locale]/ # 包圍整個本地化應用
│ ├── layout.tsx # 驗證地區,<html lang>,generateStaticParams
│ └── page.tsx
└── not-found.tsx # 全域 404 位於 [locale] 之外
使此形狀生效的規則:
[locale]是一個根動態區段。 所有本地化內容都位於其下。根層級路由(not-found.tsx、error.tsx、api/)保持在外,且不得無條件呼叫 i18n 掛鉤,否則會在執行時崩潰。- 一個來源地區。
messages/en.json是主檔。每個其他檔案都必須完全鏡像其形狀。 - 所有地區知識在
src/i18n/中。 元件從@/i18n/navigation匯入Link,永遠不從next/link匯入。新增語言只需修改一個地方。
設定(全新專案)
依此順序執行。順序很重要 — proxy.ts 依賴 routing.ts,而佈局依賴 request.ts。
- 安裝
next-intl,用createNextIntlPlugin()包裹next.config.ts。 - 使用
defineRouting建立src/i18n/routing.ts。 - 建立
src/proxy.ts,匯出createMiddleware(routing)加上預設匹配器。 - 使用
getRequestConfig解析地區,建立src/i18n/request.ts。 - 將
app/**移至app/[locale]/**下;新增generateStaticParams並驗證參數。 - 使用
createNavigation(routing)建立src/i18n/navigation.ts。 - 建立
messages/en.json並使用useTranslations取代硬編碼字串。 - 設定
<html lang>並新增本地化中繼資料及 hreflang。
每個步驟的完整檔案內容:references/next-intl-setup.md。
常見陷阱
在編寫任何 i18n 程式碼之前,請閱讀本節。這些是需要花費數小時才能解決的失敗。
setRequestLocale是舊版 API。 在 Next.js 16.3+ 上,應在getRequestConfig內部使用next/root-params讀取地區。大多數指南、教學和 LLM 訓練資料仍然教導setRequestLocale(params.locale)。它仍然可以運作,並被記錄為向後相容 — 但它是舊 API。僅在 Next < 16.3 上使用它。無論哪種方式,generateStaticParams仍然是必需的。params在 Next 15+ 上是 Promise。const {locale} = await params。將params.locale視為型別錯誤,而非執行時錯誤。- 驗證地區,否則未知值會渲染。 使用
hasLocale(routing.locales, locale)和notFound()。沒有這個,/xx/about會以虛假 URL 渲染你的英文頁面。 proxy.ts,不是middleware.ts,在 Next 16 上。middleware.ts已棄用。Proxy 僅在 Node.js 上執行 — Edge 執行時期不支援。Lingui 和 next-i18next 宣傳 Edge 安全的 proxy;如果你需要 Edge,你面臨的是真正的取捨,而不是免費的勝利。- 絕不串接翻譯後的字串。
"You have " + count + " items"在日語和阿拉伯語中不合語法。使用 ICU:"{count, plural, =1 {One item} other {# items}}"。 - 絕不在一條訊息內部使用
+或模板字面量進行插值。 使用具名佔位符—{name}—以便單詞順序可以移動。 - 日期和數字會導致水合不匹配。 伺服器和瀏覽器上的
new Date()因時區和時鐘而異。在getRequestConfig中設定timeZone,或對相對時間使用用戶端渲染。症狀:「Text content does not match server-rendered HTML」。 - 格式化取決於地區。
toLocaleString()沒有明確的地區時,會根據執行時期地區格式化,而不是請求的地區。使用useFormatter()/getFormatter()。 getRequestConfig每個請求執行一次,並由 React 快取。 不要在其中放置請求相關的狀態(購物車、用戶偏好)。僅放置與地區相關的配置。- Server Actions 和 Route Handlers 沒有根參數。
await locale()僅在 Server Components 中有效。在 actions 和 handlers 中明確將地區作為引數傳遞。
改造既有應用程式
將此視為會影響 URL 的遷移,而不是配置變更。當您新增 [locale] 前綴時,每個現有 URL 的形狀都會改變。
依此順序規劃:
- 盤點字串 — 執行
scripts/find-untranslated.sh找出硬編碼的 JSX 文字。 - 在編碼前選擇路由策略。 始終使用地區前綴(
/en/about,/de/about)是預設值;它明確且 hreflang 乾淨。localePrefix: 'as-needed'在裸路徑上提供預設地區 — URL 更好,SEO 訊號較弱。現在決定;稍後切換是完全重新遷移。 - 在接觸任何元件之前先建立路由。 讓
proxy.ts+[locale]僅使用英文訊息就能運作。 - 將路由移至
[locale]/下 — 這是一個大型機械性git mv。在一個提交中完成,以便git log --follow保持有用。 - 替換字串 按命名空間進行,而不是按檔案。端到端翻譯一個命名空間,以便在關鍵形狀擴散之前得到驗證。
- 為每個變更的 URL 新增重定向。
/about→/en/about(或 307 到偵測到的地區)。沒有這個,你將失去所有現有排名。不容妥協。 - 僅在每個地區都擁有該路由之後 才新增 hreflang + 本地化中繼資料,否則你會發出指向 404 的替代連結。
包含重定向和回滾說明的完整遷移序列:references/migration.md。
應避免的事項
- 不要將
messages/放在src/內部。 原始碼樹中的純伺服器字典會模糊哪些內容會傳送到用戶端。 - 不要在本地化路由中從
next/link匯入Link。 它會丟失地區前綴,並靜默地連結到錯誤的語言。 - 不要在 href 中手寫地區前綴(
href={/${locale}/about})。使用來自@/i18n/navigation的Link,它會處理帶前綴、按需和域名路由。 - 不要使用一個扁平的關鍵字命名空間。 按功能/頁面分組。扁平的關鍵字會衝突且無法搜尋。
- 不要將建置時自動翻譯作為真實來源。 機器輸出是草稿;未經審查就提交會使錯誤永久化且無法追溯。
- 不要只將一個地區新增到
routing.locales就稱完成了。 沒有翻譯的messages/xx.json,每個頁面都會渲染關鍵字或靜默地回退。 - 不要混合路由策略。
as-needed加上指向帶前綴 URL 的hreflang是自找的 SEO 錯誤。
參考資料
按需閱讀這些內容,而非預先閱讀:
references/library-comparison.md— 在 next-intl、Lingui、next-i18next 之間進行選擇;套件大小、遷移觸發條件、每個函式庫的設定形狀。references/next-intl-setup.md— 規範設定中的每個檔案,可直接複製貼上。references/messages-and-keys.md— 命名空間結構、關鍵字命名、ICU 語法、日期/數字/列表格式化、型別化訊息。references/seo.md— hreflang、<html lang>、本地化generateMetadata、canonical、站點地圖替代、語言切換器 SEO 規則。references/migration.md— 改造既有應用程式、重定向、回滾。
腳本
scripts/find-untranslated.sh— 標記專案中可能硬編碼的面向使用者字串,以便你可以在開始前評估工作量。在改造專案的第一步執行它。scripts/run-trigger-eval.sh— 測量此技能的description是否在正確的提示上觸發。在變更描述時使用它。
測試描述
description 是代理程式在決定載入此技能之前唯一看到的東西。如果它無法觸發,下面的內容都不重要。
bash scripts/run-trigger-eval.sh --dry-run # 顯示計畫,不進行 API 呼叫
bash scripts/run-trigger-eval.sh --split train # 12 個查詢 x 3 次執行
bash scripts/run-trigger-eval.sh --split validation --out report.json
查詢集:evals/trigger-queries.json — 12 個應觸發,8 個不應觸發,按 60/40 分割為固定的訓練集和驗證集。
編輯描述時:
- 僅使用訓練集修正失敗。最後再觸碰驗證集一次,以檢查變更是否泛化 — 如果訓練集改善而驗證集下降,則你過擬合了。
- 應觸發的失敗表示描述太窄:新增查詢所代表的概念,而不是查詢的關鍵字。
- 不應觸發的失敗表示描述太寬泛:說明此技能不涵蓋什麼。
- 編輯前重新執行。觸發是非確定性的,一次執行不能證明任何事情。
不要從失敗的查詢中新增關鍵字 — 這樣描述會看起來像關鍵字列表,並停止匹配真實提示。