Phase D #17: 修復 i18n 語系切換 Hydration 當機 問題: Client/Server 渲染語系落差導致 Hydration Mismatch 解法: Middleware 強制綁定 NEXT_LOCALE Cookie 實作內容: - 從 URL 路徑提取當前語系 - 強制設定 NEXT_LOCALE cookie (1年 TTL) - 確保 Server/Client 語系一致 @see QA Report 3.1 節 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
/**
|
||
* Next.js Middleware - i18n + Hydration 防護
|
||
* ==========================================
|
||
* Phase D #17: 修復 i18n 語系切換 Hydration 當機
|
||
*
|
||
* 問題: Client/Server 渲染語系落差導致 Hydration Mismatch
|
||
* 解法: 強制綁定 NEXT_LOCALE Cookie,確保一致性
|
||
*
|
||
* 版本: v1.1
|
||
* 建立: 2026-03-31 (台北時區)
|
||
* 建立者: Claude Code
|
||
*
|
||
* @see QA Report 3.1 節 - i18n 語系切換報錯
|
||
*/
|
||
|
||
import createMiddleware from 'next-intl/middleware'
|
||
import { NextResponse, type NextRequest } from 'next/server'
|
||
import { routing } from './i18n/routing'
|
||
|
||
// 建立 next-intl middleware
|
||
const intlMiddleware = createMiddleware(routing)
|
||
|
||
/**
|
||
* 強制綁定 NEXT_LOCALE Cookie
|
||
*
|
||
* 確保 Server/Client 語系一致,避免 Hydration Mismatch
|
||
*/
|
||
export default function middleware(request: NextRequest) {
|
||
// 先執行 next-intl middleware
|
||
const response = intlMiddleware(request)
|
||
|
||
// 從 URL 路徑提取當前語系
|
||
const pathname = request.nextUrl.pathname
|
||
const localeFromPath = routing.locales.find(
|
||
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
|
||
)
|
||
|
||
// 決定最終語系 (優先使用路徑中的語系)
|
||
const finalLocale = localeFromPath || routing.defaultLocale
|
||
|
||
// 🎯 Phase D #17: 強制設定 NEXT_LOCALE cookie
|
||
// 確保 Client-side Hydration 時使用相同語系
|
||
if (response instanceof NextResponse) {
|
||
const existingLocale = request.cookies.get('NEXT_LOCALE')?.value
|
||
|
||
// 只有當 cookie 不存在或不一致時才設定
|
||
if (existingLocale !== finalLocale) {
|
||
response.cookies.set('NEXT_LOCALE', finalLocale, {
|
||
path: '/',
|
||
maxAge: 60 * 60 * 24 * 365, // 1 年
|
||
sameSite: 'lax',
|
||
})
|
||
}
|
||
}
|
||
|
||
return response
|
||
}
|
||
|
||
export const config = {
|
||
// 匹配所有路徑,除了以下例外:
|
||
// - api 路由
|
||
// - _next 靜態檔案
|
||
// - 靜態資源 (images, fonts, etc.)
|
||
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
|
||
}
|