---
# System prepended metadata

title: Next.js 15 + next-intl 雙語系最佳實踐

---

# 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