Files
awoooi/apps/web/src/middleware.ts
OG T f25e94e8c4 fix(web): #17 i18n Hydration 防護 (NEXT_LOCALE Cookie)
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>
2026-03-31 11:18:53 +08:00

66 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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|.*\\..*).*)'],
}