# Next.js 15 + next-intl 雙語系最佳實踐指南 ###### tags: `next-intl`, `i18n`, `nextjs15`, `best-practices`, `typescript`, `internationalization` ## 🎯 專案架構 ``` src/ ├── i18n/ │ ├── routing.ts # 路由配置 │ ├── navigation.ts # 導航 API │ └── request.ts # 請求配置(含錯誤處理) ├── middleware.ts # 中介軟體 ├── app/ │ ├── layout.tsx # 根佈局 │ └── [locale]/ │ ├── layout.tsx # 語系佈局 │ └── page.tsx # 頁面 └── messages/ ├── zh.json # 中文翻譯 └── en.json # 英文翻譯 ``` ## 📝 核心配置檔案 ### 1. i18n/request.ts(官方建議的錯誤處理) ```typescript import { getRequestConfig } from 'next-intl/server'; import { hasLocale, IntlErrorCode } from 'next-intl'; import { routing } from './routing'; export default getRequestConfig(async ({ requestLocale }) => { const requested = await requestLocale; const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale; return { locale, messages: (await import(`../../messages/${locale}.json`)).default, // 官方建議的錯誤處理 onError(error) { if (error.code === IntlErrorCode.MISSING_MESSAGE) { console.warn('Missing translation:', error.message); } else { console.error('Translation error:', error); } }, // 官方建議的回退訊息 getMessageFallback({ namespace, key, error }) { const path = [namespace, key].filter((part) => part != null).join('.'); if (error.code === IntlErrorCode.MISSING_MESSAGE) { return process.env.NODE_ENV === 'development' ? `[Missing: ${path}]` : ''; } else { return process.env.NODE_ENV === 'development' ? `[Error: ${path}]` : ''; } } }; }); ``` ### 2. i18n/routing.ts ```typescript import { defineRouting } from 'next-intl/routing'; export const routing = defineRouting({ locales: ['zh', 'en'], defaultLocale: 'zh', localePrefix: 'always' }); ``` ### 3. middleware.ts ```typescript import createMiddleware from 'next-intl/middleware'; import { routing } from './i18n/routing'; export default createMiddleware(routing); export const config = { matcher: '/((?!api|_next|_vercel|.*\\..*).*)' }; ``` ## 🧩 元件統一寫法 ### ✅ 正確的官方寫法 ```typescript 'use client'; import { useTranslations, useLocale } from 'next-intl'; const ComponentName = () => { const t = useTranslations('NameSpace'); const locale = useLocale(); // 官方建議:直接使用 t() 函數,錯誤處理由 i18n/request.ts 統一管理 return ( <div> <h1>{t('title')}</h1> <p>{t('description')}</p> <span>{t('nested.key')}</span> </div> ); }; ``` ## 📋 翻譯檔案結構 ### messages/zh.json ```json { "Header": { "search": { "placeholder": "請輸入搜尋關鍵字...", "closeLabel": "關閉搜尋" }, "language": { "current": "EN" } }, "Footer": { "company": { "name": "股份有限公司" }, "contact": { "phone": "電話:", "email": "Email:", "fax": "傳真:", "address": "地址:" } } } ``` ## 🚀 最佳實踐要點 ### 1. 錯誤處理 - ✅ 使用官方的 `onError` 和 `getMessageFallback` - ✅ 開發環境顯示詳細錯誤,生產環境顯示空字串 - ❌ 不要在元件中自訂錯誤處理函數 ### 2. 元件設計 - ✅ 直接使用 `t()` 函數 - ✅ 所有元件使用統一模式 - ❌ 不要使用 `safeT` 或其他包裝函數 ### 3. 效能優化 - ✅ 使用 `generateStaticParams()` 支援靜態生成 - ✅ 使用 `setRequestLocale()` 啟用靜態渲染 - ✅ 集中化錯誤處理,避免重複代碼 ### 4. 開發體驗 - 開發環境:顯示 `[Missing: Footer.contact.phone]` - 生產環境:顯示空字串,不破壞 UI - 控制台:清晰的警告和錯誤訊息 ## 🔧 常見問題解決 1. **翻譯不顯示**:檢查 `i18n/request.ts` 是否正確返回 locale 和 messages 2. **巢狀鍵值問題**:直接使用 `t('nested.key')`,官方會自動處理 3. **錯誤處理**:統一在 `i18n/request.ts` 中配置,不要在元件中處理 ## 📊 技術棧 - Next.js 15 (App Router) - next-intl 4.3.9 - TypeScript - React 18