Next.js 如何設置 i18n (use app router)
JavaScript
- 需要先了解、設置 middleware。
- 新增 [lang] rounter。
- 設置 layout.tsx 。
- 新增 Dictionaries。
新增 middleware.ts
- 與 /src/app 同個層級。
可以看到預設語言雖然是 en-US ,但在 getLocale() 裡面會依照瀏覽器的語言設定。
若瀏覽器使用的語言是 zh-TW ,在 getLocale() 就會回傳 zh-TW 。
// src/middleware.ts import { match as matchLocale } from "@formatjs/intl-localematcher"; import Negotiator from "negotiator"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; const locales = ["en-US", "zh-TW", "zh-CN"]; const defaultLocale = "en-US"; // Get the preferred locale function getLocale(request: NextRequest): string { // Negotiator expects plain object so we need to transform headers const negotiatorHeaders: Record<string, string> = {}; request.headers.forEach((value, key) => { negotiatorHeaders[key] = value; }); // Use negotiator and intl-localematcher to get best locale const languages = new Negotiator({ headers: negotiatorHeaders }).languages( locales ); const matchedLocale = matchLocale(languages, locales, defaultLocale); return matchedLocale; } export function middleware(request: NextRequest) { // Check if there is any supported locale in the pathname const { pathname } = request.nextUrl; const pathnameHasLocale = locales.some( (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` ); if (pathnameHasLocale) return NextResponse.next(); // Redirect if there is no locale const locale = getLocale(request); request.nextUrl.pathname = `/${locale}${pathname}`; // e.g. incoming request is /products // The new URL is now /en-US/products return NextResponse.redirect(request.nextUrl); } export const config = { matcher: [ // Skip all internal paths (_next) "/((?!_next).*)", // Optional: only run on root (/) URL // '/' ], };
新增 [lang] router
- [”name”] 為 next.js 的約定檔案,可以動態產生路由。詳請看這邊
- 在 /app、/app/page 中間加入一層 [lang]
└── 📁app └── 📁[lang] └── some page...
設置 layout.tsx
- 從 props 取出 params、lang。
- 放在 html 標籤上。
import type { Metadata } from "next"; import { Roboto_Condensed } from "next/font/google"; import "./globals.css"; const roboto = Roboto_Condensed({ subsets: ["latin"] }); export const metadata: Metadata = { title: "your_title", description: "your_description", }; export default function RootLayout(props: { children: React.ReactNode; params: { lang: string }; }) { return ( <html lang={props.params.lang}> <body className={`${roboto.className} px-4 `}>{props.children}</body> </html> ); }
新增 Dictionaries
- 建立 discionaries 資料夾、底下新增欲翻譯的語言。
- 新增 get-dictionary.ts 。
新增 discionaries 資料夾
└── 📁src └── 📁dictionaries └── en.json └── zh-CN.json └── zh-TW.json
// en.json { "root":{ "welcome":"Welcome", "name":"CLMS" } }
新增一個 get-dictionary.ts 用來載入欲翻譯的語言。
// src/get-dictionary.ts import "server-only"; const dictionaries: Record<string, () => Promise<any>> = { en: () => import("./dictionaries/en.json").then((module) => module.default), "zh-TW": () => import("./dictionaries/zh-TW.json").then((module) => module.default), "zh-CN": () => import("./dictionaries/zh-CN.json").then((module) => module.default), }; export const getDictionary = async (locale: string): Promise<any> => { try { const dictionaryLoader = dictionaries[locale] ?? dictionaries["en"]; return await dictionaryLoader(); } catch (error) { console.error(`Failed to load dictionary for locale '${locale}':`, error); throw new Error(`Failed to load dictionary for locale '${locale}'`); } };
use it!
import { getDictionary } from "@/get-dictionary"; import Login from "../../components/login/login"; interface Props { params: { lang: string; }; } export default async function Page({ params: { lang } }: Props) { const dict = await getDictionary(lang); return ( <div className="flex flex-col items-center mt-40"> <h1 className="my-4 "> {dict.root.welcome} <span className="text-blue-500">{dict.root.name}</span> </h1> <Login /> </div> ); }