載入與初始設置 next-intl === ###### With Next.js App Router :::info :radio_button: [回到目錄攻略](https://hackmd.io/@hJJ8etrATgudRfKA1gryew/BJiplPwMC) ::: 0.多國語系套件選擇 --- 選擇要使用哪個多國語套件也是一項難題,其實Next.js本身也有[內建多國語系](https://nextjs.org/docs/app/building-your-application/routing/internationalization),但無法做客製化的參數,著實腦人。經過幾番探索與研究,最後選擇==next-intl==作為本次專案的多國語系套件。 :round_pushpin:入坑參考資料: [How to Choose an i18n Library for a Next.js 14 Application](https://blog.stackademic.com/how-to-choose-an-i18n-library-for-a-next-js-14-application-part-1-861ec8889128) 1.next-intl套件下載 --- 在Terminal輸入指令: ``` npm install next-intl ``` 2.目錄結構 --- ``` ├── messages (1) │ ├── en │ │ ├──1_global.json │ │ └── ... │ └── zh │ ├──1_global.json │ └── ... ├── next.config.js (2) └── src ├── lib (3) │ ├──config.ts │ ├──i18n.ts │ └──navigation.ts ├── middleware.ts (4) └── app └── [locale] ├── layout.tsx (5) └── main (6,主網頁) └── page.tsx ``` 與[next-intl官網](https://next-intl-docs.vercel.app/docs/getting-started/app-router)提供的參考架構有些不一樣,主要的不同之處如下: - ```messages```下各建立不同語言的資料夾,使得能夠依網頁的不同而做語言分類。 - ```src```下再建立```lib```資料夾,將各種關於語言同步化、語言載入媒介等工具集中管理於此。 - 原本在`loclae`裡有直接夾帶門面`page.tsx`,但基於本專案有分主網站與子網站,所以門面的網站必須帶有主網站名稱,這也就先刪掉原本的門面。 3.初建語言檔案 --- > @/messages/en/1_global.json ```json= { "main-title": "Welcome to Taiwan!", "not-found-title": "404 NOT FOUND!", "not-found-return": "RETURN" } ``` > @/messages/zh/1_global.json ```json= { "main-title": "歡迎來台灣!", "not-found-title": "404 頁面找不到啦哭!", "not-found-return": "返回" } ``` 4.初設在地化語言/路徑配置 --- > @src/lib/config.ts ```typescript= import { Pathnames } from "next-intl/navigation"; export const locales = ["en", "zh"] as const; /// 鎖死不同語言對應的網址 export const pathnames = { "/main": "/main", } satisfies Pathnames<typeof locales>; /// 前綴都要帶語言,例如:en/main、zh/main export const localePrefix = "always"; export type AppPathnames = keyof typeof pathnames; ``` > @src/lib/navigation.ts ```typescript= import { createLocalizedPathnamesNavigation } from "next-intl/navigation"; import { localePrefix, locales, pathnames } from "./config"; // 導入所需變數,內建自己的導航系統 export const { Link, redirect, usePathname, useRouter } = createLocalizedPathnamesNavigation({ locales, localePrefix, pathnames, }); ``` 兩個檔案建置好後,即能確保在點選其他網址後,也能同步更新語言。 5.建置語言載入媒介 --- > @src/lib/i18n.ts ```typescript= import { getRequestConfig } from "next-intl/server"; import { notFound } from "next/navigation"; import { locales } from "./config"; export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale as any)) notFound(); const messages = { ...(await import(`@/messages/${locale}/1_global.json`)).default, }; return { messages, }; }); ``` 6.導入中介軟體 --- middleware.ts為轉換語言的媒介,而本專案所設的**預設語言**,<font color="#AC19C9">主要是吃瀏覽器的語言喜好</font>,因此要先載入**判斷瀏覽器語言喜好的套件-negotiator**。 在撰寫middleware.ts前,就先在Terminal輸入指令吧: ``` npm install negotiator @types/negotiator ``` 載好後就開始寫吧! > @src/middleware.ts ```typescript= import { match as matchLocale } from "@formatjs/intl-localematcher"; import { localePrefix, locales, pathnames } from "@lib/config"; import Negotiator from "negotiator"; import createIntlMiddleware from "next-intl/middleware"; import { NextRequest } from "next/server"; function getLocale(request: NextRequest): string | undefined { const negotiatorHeaders: Record<string, string> = {}; request.headers.forEach((value, key) => (negotiatorHeaders[key] = value)); // @ts-ignore locales are readonly const setLocales: string[] = locales; const setDefaultLocale: string = "en"; // languages為瀏覽器設定語言喜好順序陣列 const languages = new Negotiator({ headers: negotiatorHeaders }).languages(); // matchLocale會做陣列比對,取最多次出現且順序第一的語言 const locale = matchLocale(languages, setLocales, setDefaultLocale); return locale; } export default async function middleware(request: NextRequest) { const locale = getLocale(request); const defaultLocale = locale === "zh" || locale === "zh-TW" ? "zh" : "en"; const handleI18nRouting = createIntlMiddleware({ locales, defaultLocale, localePrefix, localeDetection: false, pathnames, }); const response = handleI18nRouting(request); return response; } export const config = { // Match only internationalized pathnames matcher: [ // Enable a redirect to a matching locale at the root "/", // Set a cookie to remember the previous locale for // all requests that have a locale prefix "/(zh|en)/:path*", // Enable redirects that add missing locales // (e.g. `/pathnames` -> `/en/pathnames`) "/((?!api|_next|_vercel|.*\\..*).*)", ], }; ``` 這樣寫好後,就會根據瀏覽器的語言去顯示網頁預設語言了! 進一步說,本網站只有中文/英文去做轉換,因此只有喜好語言將中文設為最前面時(再往前沒有英文),其預設語言便會為中文;而其他情況下,預設語言就都會是英文。假設情境如下: :::info :bulb:[ 情境一 ] **瀏覽器喜好語言排序:中 > 英 > 西 > 日** **所得出的預設語言為:zh** ::: :::info :bulb:[ 情境二 ] **瀏覽器喜好語言排序:西 > 中 > 日** **所得出的預設語言為:zh** ::: :::info :bulb:[ 情境三 ] **瀏覽器喜好語言排序:西 > 日** **所得出的預設語言為:en** ::: :::info :bulb:[ 情境四 ] **瀏覽器喜好語言排序:西 > 日 > 英 > 中** **所得出的預設語言為:en** ::: 7.設定路徑管理 --- > @/next.config.js ```javascript= // @ts-check // 向伺服器元件提供 i18n 配置 const withNextIntl = require("next-intl/plugin")("./src/lib/i18n.ts"); /** * @type {import('next').NextConfig} */ const nextConfig /** @type {import('next').NextConfig} */ = { async redirects() { return [ { source: "/", permanent: false, destination: "/main", }, { source: "/en", permanent: false, destination: "/en/main", }, { source: "/zh", permanent: false, destination: "/zh/main", }, ]; }, }; // @ts-ignore module.exports = withNextIntl(nextConfig); ``` 透過```redirects```撰寫預設網址,<font color="#AC19C9">不管是有沒有帶語言前綴,都可以直接導向主網頁</font>。 8.結果呈現 --- 中英文切換的樣子,以及網址的呈現: ![20240508_093227](https://hackmd.io/_uploads/By9FL8uzR.gif) 參考 --- - [NEXT.JS: Getting Started](https://next-intl-docs.vercel.app/docs/getting-started/app-router) - [Navigation APIs: Localized pathnames](https://next-intl-docs.vercel.app/docs/routing/navigation#localized-pathnames)