Next.js14 App Router配置国际化

Published on
Reading time
8 min read
Authors

Next.js14 App Router配置国际化

前言

自从 nextjs 14 推出了 App Router 这个新特性,在 Page Router 下国际化路由功能在 App Router 中已被移除, 下文将介绍如何在 Next.js14 中配置 App Router 实现国际化。

使用到的插件

next-i18n-router

默认情况下,next-i18n-router 会解析请求的 Accept-Language 头部来确定用户偏好的语言环境,并根据设置重定向或重写 URL。

i18next + react-i18next

react-i18next 是基于 i18next 框架构建的一个库,专门用于在 React 应用中实现国际化和本地化功能。i18next 提供了核心的国际化功能,而 react-i18next 则为这些功能在 React 环境中的应用提供了便捷的接口和组件。

i18next-resources-to-backend

i18next-resources-to-backend 是一个用于 i18next 的插件,它允许开发者从远程资源(例如服务器或 API)动态加载翻译文件。

这个插件搭配 rsc ,比如在应用启动后从服务器拉取最新的翻译数据然后填充到 html 中,避免在初始加载时包含所有可能的翻译文件,从而减少应用的初始大小。

开始配置

安装依赖

bashEavan.dev
npm install i18next react-i18next i18next-resources-to-backend next-i18n-router

配置路由

需要用到next-i18n-router这个插件

这个插件的原理是在nextjs的路由中间件中根据请求头的Accept-Language来判断用户的语言偏好,然后根据配置的语言列表和默认语言来重定向或重写URL。

比如用户请求的URL是 /zh/about ,如果用户的语言偏好是英文,那么会重定向到 /en/about

browser accept language
其中的 zh,en是对应的语言标识,q=0.9表示权重,权重越高,表示用户对这个语言的偏好越高。
  1. 首先创建 config 配置 在项目根目录创建 i18nConfig.ts
typescriptEavan.dev
const i18nConfig = {
  locales: ['en', 'zh'], // 支持的语言列表
  defaultLocale: 'en', // 默认语言
}

export default i18nConfig
  1. 配置路由动态 path 在 app 目录下创建 [locale] 目录,然后将之前 app 目录下的文件全部移动到 [locale] 目录下
shellEavan.dev
└── app
    └── [locale]
        ├── layout.js
        └── page.js
  1. 创建路由中间件 在项目根目录创建 middleware.ts 文件,如果配置文件地址不同,请自行修改
typescriptEavan.dev
import { i18nRouter } from 'next-i18n-router'
import i18nConfig from '../i18nConfig' // 引入配置文件
import { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  return i18nRouter(request, i18nConfig)
}

// 配置只适用于中间件的路由
export const config = {
  matcher: '/((?!api|static|.*\\..*|_next).*)',
}

配置翻译文件

在项目根目录下创建 locales 目录,然后在 locales 目录下创建 enzh 目录,分别存放英文和中文的翻译文件, 对应的文件名 common 是命名空间,可以根据实际情况进行修改。

shellEavan.dev
└── src
    └── locales
        ├── en
        │   └── common.json
        └── zh
            └── common.json

对应文件内容如下

en/common.json

jsonEavan.dev
{
  "msg": "Hello, World!"
}

zh/common.json

jsonEavan.dev
{
  "msg": "你好,世界!"
}

加载翻译文件

在项目根目录下创建 i18n.ts 文件,请根据实际情况修改对应路径

生成一个 i18next 实例,该实例将用于加载和管理翻译文件。然后使用 i18next-resources-to-backend 插件从远程资源加载翻译文件。

typescriptEavan.dev
import { Resource, createInstance, i18n } from 'i18next'
import { initReactI18next } from 'react-i18next/initReactI18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import i18nConfig from '../../i18nConfig' // 引入之前的配置文件,根据实际情况修改路径

export default async function initTranslations(
  locale: string,
  namespaces?: string[],
  i18nInstance?: i18n,
  resources?: Resource
) {
  namespaces = namespaces || ['common']

  // locale = locale || i18nConfig.defaultLocale;

  i18nInstance = i18nInstance || createInstance()

  i18nInstance.use(initReactI18next)

  if (!resources) {
    i18nInstance.use(
      resourcesToBackend(
        (language: string, namespace: string) => import(`@/locales/${language}/${namespace}.json`)
      )
    )
  }

  await i18nInstance.init({
    lng: locale,
    resources,
    fallbackLng: i18nConfig.defaultLocale,
    supportedLngs: i18nConfig.locales,
    defaultNS: namespaces[0],
    fallbackNS: namespaces[0],
    ns: namespaces,
    preload: resources ? [] : i18nConfig.locales,
  })

  return {
    i18n: i18nInstance,
    resources: i18nInstance.services.resourceStore.data,
    t: i18nInstance.t,
  }
}

在服务端加载翻译文件

导入上一步的配置文件,根据实际情况修改路径

tsxEavan.dev
import initTranslations from '@/i18n' // 引入之前的配置文件,根据实际情况修改路径

export default async function Home({ params: { locale } }) {
  const { t } = await initTranslations(locale)

  return (
    <main>
      <h1>{t('msg')}</h1>
    </main>
  )
}

在客户端加载翻译文件

单独新建一个Provider组件,用于客户端加载翻译文件

tsxEavan.dev
'use client'

import { I18nextProvider } from 'react-i18next'
import { ReactNode } from 'react'
import initTranslations from '@/i18n'
import { createInstance, Resource } from 'i18next'

export interface TranslationsProviderProps {
  children: ReactNode
  locale: string
  namespaces?: string[]
  resources?: Resource
}

export default function TranslationsProvider({
  children,
  locale,
  namespaces,
  resources,
}: TranslationsProviderProps) {
  const i18n = createInstance()

  initTranslations(locale, namespaces, i18n, resources)

  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>
}

引用组件加载翻译文件,尽管我们用这个客户端组件包裹了页面,但页面仍作为服务端组件进行渲染

tsxEavan.dev
import initTranslations from '../i18n'
import TranslationsProvider from '@/components/TranslationsProvider'

const i18nNamespaces = ['common']

export default async function Home({ params: { locale } }) {
  const { t, resources } = await initTranslations(locale, i18nNamespaces)

  return (
    <TranslationsProvider namespaces={i18nNamespaces} locale={locale} resources={resources}>
      <main>
        <h1>{t('header')}</h1>
      </main>
    </TranslationsProvider>
  )
}

在客户端组件中使用翻译文件

tsxEavan.dev
'use client'

import { useTranslation } from 'react-i18next'

export default function ExampleClientComponent() {
  const { t } = useTranslation()

  return <h3>{t('msg')}</h3>
}

配置 layout

最后为了更好的seo 和无障碍等,我们需要在 html标签上添加 lang 和 dir。

html cnhtml cn

修改app/[locale]/layout.tsx

tsxEavan.dev
import { Inter } from 'next/font/google'
import '../globals.css'
import i18nConfig from '@/i18n/i18nConfig'
import { dir } from 'i18next'

const inter = Inter({ subsets: ['latin'] })

export function generateStaticParams() {
  return i18nConfig.locales.map((locale) => ({ locale }))
}

export default function RootLayout({
  children,
  params: { locale },
}: Readonly<{
  children: React.ReactNode
  params: { locale: string }
}>) {
  return (
    <html lang={locale} dir={dir(locale)}>
      <body className={inter.className}>{children}</body>
    </html>
  )
}

配置 WebStorm 支持

安装对应的 i18n 插件, 然后在 WebStorm 中配置对应的翻译文件路径

IDE support

这样不仅可以在代码中直接使用翻译文件的 key,还可以在 WebStorm 中直接查看翻译文件的内容。

IDE support