# 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