用i18next使你的应用国际化-Next.js(App router)

安装插件

npm install i18next react-i18next i18next-resources-to-backend

1. 目录结构

.
└── app
    └── [lng]
        ├── second-page
        |   └── page.js
        ├── layout.js
        └── page.js

app/[lng]/page.js文件:

import Link from 'next/link'

export default function Page({ params: { lng } }) {
  return (
    <>
      

Hi there!

second page ) }

app/[lng]/second-page/page.js文件:

import Link from 'next/link'

export default function Page({ params: { lng } }) {
  return (
    <>
      

Hi from second page!

back ) }

app/[lng]/layout.js文件:

import { dir } from 'i18next'

const languages = ['en', 'de']

export async function generateStaticParams() {
  return languages.map((lng) => ({ lng }))
}

export default function RootLayout({
  children,
  params: {
    lng
  }
}) {
  return (
    
      
      
        {children}
      
    
  )
}

2. 语言识别

现在导航到http://localhost:3000/enhttp://localhost:3000/de应该显示一些东西,并且到第二页和返回的链接也应该生效,但是导航到http://localhost:3000将返回404错误。

为了解决这个问题,我们将创建一个Next.js中间件并重构一些代码:

创建app/i18n/settings.js文件:

export const fallbackLng = 'en'
export const languages = [fallbackLng, 'de']

修改app/[lng]/layout.js文件:

import { dir } from 'i18next'
import { languages } from '../i18n/settings'

export async function generateStaticParams() {
  return languages.map((lng) => ({ lng }))
}

export default function RootLayout({
  children,
  params: {
    lng
  }
}) {
  return (
    
      
      
        {children}
      
    
  )
}

创建middleware.js文件:

npm install accept-language
import { NextResponse } from 'next/server'
import acceptLanguage from 'accept-language'
import { fallbackLng, languages } from './app/i18n/settings'

acceptLanguage.languages(languages)

export const config = {
  // matcher: '/:lng*'
  matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)']
}

const cookieName = 'i18next'

export function middleware(req) {
  let lng
  if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName).value)
  if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'))
  if (!lng) lng = fallbackLng

  // Redirect if lng in path is not supported
  if (
    !languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith('/_next')
  ) {
    return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
  }

  if (req.headers.has('referer')) {
    const refererUrl = new URL(req.headers.get('referer'))
    const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`))
    const response = NextResponse.next()
    if (lngInReferer) response.cookies.set(cookieName, lngInReferer)
    return response
  }

  return NextResponse.next()
}

用i18next使你的应用国际化-Next.js(App router)_第1张图片

现在导航到根路径/首先会检查是否已经有上次选择的语言的cookie,若没有,作为回退将检查Accept-Language header ,若仍然没有,最后回退是自定义的回退语言。

检测到的语言将用于重定向到适当的页面。

3. i18n instrumentation

app/i18n/index.js文件中准备i18next:

这里没有使用i18next单例,而是在每个useTranslation调用上创建一个新实例,因为在编译期间,一切似乎都是并行执行的。拥有单独的实例将保持翻译的一致性。

import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import { getOptions } from './settings'

const initI18next = async (lng, ns) => {
  const i18nInstance = createInstance()
  await i18nInstance
    .use(initReactI18next)
    .use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`)))
    .init(getOptions(lng, ns))
  return i18nInstance
}

export async function useTranslation(lng, ns, options = {}) {
  const i18nextInstance = await initI18next(lng, ns)
  return {
    t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options.keyPrefix),
    i18n: i18nextInstance
  }
}

app/i18n/settings.js文件中,我们将添加i18next选项:

...
export const defaultNS = 'translation'

export function getOptions (lng = fallbackLng, ns = defaultNS) {
  return {
    // debug: true,
    supportedLngs: languages,
    fallbackLng,
    lng,
    fallbackNS: defaultNS,
    defaultNS,
    ns
  }
}

准备一些翻译文件:

.
└── app
    └── i18n
        └── locales
            ├── en
            |   ├── translation.json
            |   └── second-page.json
            └── de
                ├── translation.json
                └── second-page.json

app/i18n/locales/en/translation.json:

{
  "title": "Hi there!",
  "to-second-page": "To second page"
}

app/i18n/locales/de/translation.json:

{
  "title": "Hallo Leute!",
  "to-second-page": "Zur zweiten Seite"
}

app/i18n/locales/en/second-page.json:

{
  "title": "Hi from second page!",
  "back-to-home": "Back to home"
}

app/i18n/locales/de/second-page.json:

{
  "title": "Hallo von der zweiten Seite!",
  "back-to-home": "Zurück zur Hauptseite"
}

现在准备在页面中使用它…

服务器页面可以通过async方式等待useTranslation响应。

app/[lng]/page.js:

import Link from 'next/link'
import { useTranslation } from '../i18n'

export default async function Page({ params: { lng } }) {
  const { t } = await useTranslation(lng)
  return (
    <>
      

{t('title')}

{t('to-second-page')} ) }

app/[lng]/second-page/page.js:

import Link from 'next/link'
import { useTranslation } from '../../i18n'

export default async function Page({ params: { lng } }) {
  const { t } = await useTranslation(lng, 'second-page')
  return (
    <>
      

{t('title')}

{t('back-to-home')} ) }

用i18next使你的应用国际化-Next.js(App router)_第2张图片

4. 语言切换器

在Footer组件中定义一个语言切换器:

app/[lng]/components/Footer/index.js:

import Link from 'next/link'
import { Trans } from 'react-i18next/TransWithoutContext'
import { languages } from '../../../i18n/settings'
import { useTranslation } from '../../../i18n'

export const Footer = async ({ lng }) => {
  const { t } = await useTranslation(lng, 'footer')
  return (
    
Switch from {{lng}} to:{' '} {languages.filter((l) => lng !== l).map((l, index) => { return ( {index > 0 && (' or ')} {l} ) })}
) }

上述代码中使用了react-i18next Trans组件和新的命名空间:

app/i18n/locales/en/footer.json:

{
  "languageSwitcher": "Switch from <1>{{lng}} to: "
}

app/i18n/locales/de/footer.json:

{
  "languageSwitcher": "Wechseln von <1>{{lng}} nach: "
}

将Footer组件添加到页面:

app/[lng]/page.js:

...
import { Footer } from './components/Footer'

export default async function Page({ params: { lng } }) {
  ...
  return (
    <>
      ...
      
) }

app/[lng]/second-page/page.js:

...
import { Footer } from '../components/Footer'

export default async function Page({ params: { lng } }) {
  ...
  return (
    <>
      ...
      
) }

用i18next使你的应用国际化-Next.js(App router)_第3张图片

5. 客户端

到目前为止,我们只创建了服务器端页面。

那么客户端页面是什么样的呢?

由于客户端react组件不能async,我们需要做一些调整。

介绍一下app/i18n/client.js文件:

'use client'

import { useEffect, useState } from 'react'
import i18next from 'i18next'
import { initReactI18next, useTranslation as useTranslationOrg } from 'react-i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
import { getOptions, languages } from './settings'

const runsOnServerSide = typeof window === 'undefined'

// 在客户端,正常的单例模式是可以的
i18next
  .use(initReactI18next)
  .use(LanguageDetector)
  .use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`)))
  .init({
    ...getOptions(),
    lng: undefined, // 在客户端检测语言
    detection: {
      order: ['path', 'htmlTag', 'cookie', 'navigator'],
    },
    preload: runsOnServerSide ? languages : []
  })

export function useTranslation(lng, ns, options) {
  const ret = useTranslationOrg(ns, options)
  const { i18n } = ret
  if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
    i18n.changeLanguage(lng)
  } else {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage)
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useEffect(() => {
      if (activeLng === i18n.resolvedLanguage) return
      setActiveLng(i18n.resolvedLanguage)
    }, [activeLng, i18n.resolvedLanguage])
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useEffect(() => {
      if (!lng || i18n.resolvedLanguage === lng) return
      i18n.changeLanguage(lng)
    }, [lng, i18n])
  }
  return ret
}

在客户端,正常的i18next单例就可以了。它将只初始化一次。我们可以使用“普通的”useTranslation钩子。我们只是包装它,以便有可能传递语言。

为了与服务器端语言检测保持一致,我们使用i18next-browser-languagedetector并相应地配置它。

我们还需要创建2个版本的Footer组件。

.
└── app
    └── [lng]
        └── components
            └── Footer
                ├── client.js
                ├── FooterBase.js
                └── index.js

app/[lng]/components/Footer/FooterBase.js:

import Link from 'next/link'
import { Trans } from 'react-i18next/TransWithoutContext'
import { languages } from '../../../i18n/settings'

export const FooterBase = ({ t, lng }) => {
  return (
    
Switch from {{lng}} to:{' '} {languages.filter((l) => lng !== l).map((l, index) => { return ( {index > 0 && (' or ')} {l} ) })}
) }

服务器端部分继续使用async版本,app/[lng]/components/Footer/index.js:

import { useTranslation } from '../../../i18n'
import { FooterBase } from './FooterBase'

export const Footer = async ({ lng }) => {
  const { t } = await useTranslation(lng, 'footer')
  return 
}

客户端部分将使用新的i18n/client版本,app/[lng]/components/Footer/client.js:

'use client'

import { useTranslation } from '../../../i18n/client'
import { FooterBase } from './FooterBase'

export const Footer = ({ lng }) => {
  const { t } = useTranslation(lng, 'footer')
  return 
}

客户端页面看起来像这样- app/[lng]/client-page/page.js:

'use client'

import Link from 'next/link'
import { useTranslation } from '../../i18n/client'
import { Footer } from '../components/Footer/client'
import { useState } from 'react'

export default function Page({ params: { lng } }) {
  const { t } = useTranslation(lng, 'client-page')
  const [counter, setCounter] = useState(0)
  return (
    <>
      

{t('title')}

{t('counter', { count: counter })}

) }

翻译源:

app/i18n/locales/en/client-page.json:

{
  "title": "Client page",
  "counter_one": "one selected",
  "counter_other": "{{count}} selected",
  "counter_zero": "none selected",
  "back-to-home": "Back to home"
}

app/i18n/locales/de/client-page.json:

{
  "title": "Client Seite",
  "counter_one": "eines ausgewählt",
  "counter_other": "{{count}} ausgewählt",
  "counter_zero": "keines ausgewählt",
  "back-to-home": "Zurück zur Hauptseite"
}

在初始页面app/[lng]/page.js新增一个链接:

...

export default async function Page({ params: { lng } }) {
  ...
  return (
    <>
      

{t('title')}

{t('to-second-page')}
{t('to-client-page')}
) }

翻译源:

app/i18n/locales/en/translation.json:

{
  "title": "Hi there!",
  "to-second-page": "To second page",
  "to-client-page": "To client page"
}

app/i18n/locales/de/translation.json:

{
  "title": "Hallo Leute!",
  "to-second-page": "Zur zweiten Seite",
  "to-client-page": "Zur clientseitigen Seite"
}

用i18next使你的应用国际化-Next.js(App router)_第4张图片

完整代码

你可能感兴趣的:(javascript,前端,i18next,next.js)