使用 Next.js 14 實作多國語系i18n


June 22, 2024 程式語言

使用 Next.js 14 實作多國語系i18n


使用 Next.js 14 與 negotiator 實作多國語系i18n

前言

🔗

本文介紹如何在 Next.js 專案中設置多語系功能,包含設置預設語言、語系資料的管理與讀取、以及根據使用者請求語系動態呈現對應內容。範例中預設語言為英文(en),並支持繁體中文(zh-tw)。

設置步驟

🔗

1. 定義語系配置

🔗

在根目錄建立 i18n.config.ts

ts
export const i18n = {
    defaultLocale: 'en',
    locales: ['en', 'zh-tw']
  } as const
  
export type Locale = (typeof i18n)['locales'][number]

設置網站的語言,與預設語言。

2. 語系資料文件

🔗

建立各個語言對應的顯示資訊(依據自己的需求)
src\dictionaries\zh-tw.json
src\dictionaries\en.json

範例:

JSON
{
  "layout": {
    "brand": "Sun Note",
    "recentposts": "Recent Posts",
    "readmore": "Read more",
    "allposts": "All POSTS",
    "recentprojects": "Recent Projects",
    "categories": "CATEGORIES",
    "tags": "TAGS",
    "lastupdated":"Last updated"
  }
}

之後就是依據 key 去顯示對應的語系資料。

3. 取得語系資料的函數

🔗

建立取得語言內容的函數

src\lib\dictionary.ts

ts
import 'server-only'
import type { Locale } from '../../i18n.config'

const dictionaries: { [key: string]: () => Promise<any> } = {
  'en': () => import('@/dictionaries/en.json').then(module => module.default),
  'zh-tw': () => import('@/dictionaries/zh-tw.json').then(module => module.default)
}

export const getDictionary = async (locale: Locale) => dictionaries[locale]()

接下來只要在想要有多語系功能的頁面,使用getDictionary()就能取得對應語系的資料。

JS
import { getDictionary } from '@/lib/dictionary'

const { layout } = await getDictionary(lang);

4. Next.js 動態路由

🔗

至於要如何取得語系資料,可以透過網站路徑取得,使用 Next.js 動態路由,建立 [lang]src\app\[lang],實際專案結構變為:

lua
my-next-app/
├── node_modules/
├── public/
│   ├── favicon.ico
│   └── vercel.svg
├── src/
│   ├── app/
│   │   └── [lang]
│   │        ├── globals.css
│   │        ├── layout.tsx
│   │        └── page.tsx
│   ├── lib/
│   │   └── dictionary.ts
│   ├── components/
│   └── middleware.ts
├── .gitignore
├── i18n.config.ts
├── package.json
├── README.md
└── next.config.js

之後 [lang] 底下的頁面就能藉由 URL 取得對應的語系。

5. 讀取使用者預設語言

🔗

若要判斷使用者慣用系統的語言,並顯示對應的資料可以使用 negotiator 從 Header 取得語言資訊。

需在中介軟體使用,建立 src\middleware.ts

代碼

JS
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { i18n } from '../i18n.config'
import { match as matchLocale } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

/**
 * 從請求標頭中獲取首選語言
 * @param request - 傳入的請求
 * @returns 匹配的語言或 undefined
 */
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 locales: string[] = i18n.locales

    const languages = new Negotiator({ headers: negotiatorHeaders }).languages()

    const locale = matchLocale(languages, locales, i18n.defaultLocale)

    return locale
}
/**
 * 中間件函數處理語言重定向
 * @param request - 傳入的請求
 * @returns 如果需要則返回重定向的回應
 */
export function middleware(request: NextRequest) {
  
    // Check if there is any supported locale in the pathname
    const { pathname } = request.nextUrl
    const pathnameHasLocale = i18n.locales.some(
        (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
    )

    if (pathnameHasLocale) {
        if(pathname.startsWith('/en')){
            const newPathname = pathname.replace(/^\/en/, '');
            request.nextUrl.pathname = newPathname
            return NextResponse.redirect(request.nextUrl)
        }
        return
    }
    // Rewrite to en/ content if there is no locale
    // const locale = getLocale(request)
    request.nextUrl.pathname = `/${i18n.defaultLocale}${pathname}`
    return NextResponse.rewrite(request.nextUrl)
}

export const config = {
    // Matcher ignoring `/_next/` and `/api/`
    matcher: ['/((?!api|_next/static|img|pics|_next/image|favicon.ico).*)']
}

詳細解釋:

getLocale() 可以藉由 request 取得使用者的慣用語系,若與本網站不匹配,則回傳預設語系。

middleware 的邏輯主要就是依據 request 判斷 URL 是否有包含語系,若語系與網站提供的語系不相符,則 redirect 至預設語系。

因為本網站的預設語系為英文 en,不希望 en 顯示在 URL 上,所以有做一些修改,若 URL 有包含 en 則 redirect 至無語系的 URL 狀態,若原本 URL 就沒有語系的資訊則 rewrite en 對應的內容,整個中介軟體並沒有使用 getLocale(),因為我希望能透過 URL 就能顯示對應的內容,不要做跳轉。

結論

🔗

透過以上方法以實現多語系支持,並額外補充依據使用者請求語系動態呈現對應內容,提升用戶體驗。最後也在中介軟體稍微做了一點修改讓使用者即便透過無夾帶語系的 URL 訪問本站,也可以顯示預設的語系內容不會找不到頁面。

參考

Next.jsReactWeb



Avatar

Alvin

軟體工程師,喜歡金融知識、健康觀念、心理哲學、自助旅遊與系統設計。

相關文章






留言區 (0)



  or   

尚無留言