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

news2024/11/24 16:21:12

安装插件

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 (
    <>
      <h1>Hi there!</h1>
      <Link href={`/${lng}/second-page`}>
        second page
      </Link>
    </>
  )
}

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

import Link from 'next/link'

export default function Page({ params: { lng } }) {
  return (
    <>
      <h1>Hi from second page!</h1>
      <Link href={`/${lng}`}>
        back
      </Link>
    </>
  )
}

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 (
    <html lang={lng} dir={dir(lng)}>
      <head />
      <body>
        {children}
      </body>
    </html>
  )
}

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 (
    <html lang={lng} dir={dir(lng)}>
      <head />
      <body>
        {children}
      </body>
    </html>
  )
}

创建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()
}

现在导航到根路径/首先会检查是否已经有上次选择的语言的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 (
    <>
      <h1>{t('title')}</h1>
      <Link href={`/${lng}/second-page`}>
        {t('to-second-page')}
      </Link>
    </>
  )
}

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 (
    <>
      <h1>{t('title')}</h1>
      <Link href={`/${lng}`}>
        {t('back-to-home')}
      </Link>
    </>
  )
}

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 (
    <footer style={{ marginTop: 50 }}>
      <Trans i18nKey="languageSwitcher" t={t}>
        Switch from <strong>{{lng}}</strong> to:{' '}
      </Trans>
      {languages.filter((l) => lng !== l).map((l, index) => {
        return (
          <span key={l}>
            {index > 0 && (' or ')}
            <Link href={`/${l}`}>
              {l}
            </Link>
          </span>
        )
      })}
    </footer>
  )
}

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

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

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

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

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

将Footer组件添加到页面:

app/[lng]/page.js:

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

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

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

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

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

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 (
    <footer style={{ marginTop: 50 }}>
      <Trans i18nKey="languageSwitcher" t={t}>
        Switch from <strong>{{lng}}</strong> to:{' '}
      </Trans>
      {languages.filter((l) => lng !== l).map((l, index) => {
        return (
          <span key={l}>
            {index > 0 && (' or ')}
            <Link href={`/${l}`}>
              {l}
            </Link>
          </span>
        )
      })}
    </footer>
  )
}

服务器端部分继续使用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 <FooterBase t={t} lng={lng} />
}

客户端部分将使用新的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 <FooterBase t={t} lng={lng} />
}

客户端页面看起来像这样- 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 (
    <>
      <h1>{t('title')}</h1>
      <p>{t('counter', { count: counter })}</p>
      <div>
        <button onClick={() => setCounter(Math.max(0, counter - 1))}>-</button>
        <button onClick={() => setCounter(Math.min(10, counter + 1))}>+</button>
      </div>
      <Link href={`/${lng}`}>
        <button type="button">
          {t('back-to-home')}
        </button>
      </Link>
      <Footer lng={lng} />
    </>
  )
}

翻译源:

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 (
    <>
      <h1>{t('title')}</h1>
      <Link href={`/${lng}/second-page`}>
        {t('to-second-page')}
      </Link>
      <br />
      <Link href={`/${lng}/client-page`}>
        {t('to-client-page')}
      </Link>
      <Footer lng={lng}/>
    </>
  )
}

翻译源:

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"
}

完整代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/785534.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

新增WebDB和ChatGPT组件,支持对ChatGPT资产进行纳管,JumpServer堡垒机v3.5.0发布

2023年7月24日&#xff0c;JumpServer开源堡垒机正式发布v3.5.0版本。在这一版本中&#xff0c;新生代数据库连接组件——问题终结者Chen强势来袭&#xff0c;替代原有的OmniDB组件&#xff0c;在兼容旧版本的同时&#xff0c;解决了旧组件性能不足的问题&#xff0c;为用户提供…

Matlab进阶绘图第23期—密度散点图

密度散点图本质上是一种特征渲染的散点图&#xff0c;其颜色表示某一点所在区域的密度信息。 除了作图&#xff0c;密度散点图绘制的关键还在于密度的计算。 当然&#xff0c;不管是作图还是密度的计算&#xff0c;这些在《Matlab论文插图绘制模板》和《Matlab点云处理及可视…

什么是PostgreSQL?简要介绍其主要特点和用途

PostgreSQL是一种开源的关系型数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;它是最强大和广泛使用的开源数据库之一。PostgreSQL的名称起源于其前身&#xff0c;称为"Ingres"项目&#xff0c;后来被命名为Postgres&#xff0c;而PostgreSQL则是它的进一步…

tinkerCAD案例:7.Skull Button 骷髅纽扣

tinkerCAD案例&#xff1a;7.Skull Button 骷髅纽扣 In this lesson you will learn to make a skull shaped button. Let’s get started! 在本课中&#xff0c;您将学习制作一个骷髅形按钮。让我们开始吧&#xff01; 说明 Drag a Cylinder shape to the workplane. 将“圆柱…

ABAP中截取字符串中间位数常规方法

问题&#xff1a;截取字符串“SNTY2TI 071082241AF”中07108&#xff0c;常规字符串截取方法。 这里直接上代码&#xff1a; REPORT zsy_zm_test19. DATA:lv_datum TYPE sy-datum VALUE 20230428,lv_datum2 TYPE sy-datum VALUE 20220522. DATA:lv_num TYPE i. DATA:lv_strin…

Python基于PyTorch实现卷积神经网络分类模型(CNN分类算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 卷积神经网络&#xff0c;简称为卷积网络&#xff0c;与普通神经网络的区别是它的卷积层内的神经元只覆…

三个月诞生79个基础大模型,企业选用大模型需要注意些什么?

自从ChatGPT横空出世&#xff0c;各类大模型层出不穷&#xff0c;竞争也日渐激烈&#xff0c;可谓“乱花渐欲迷人眼”。 随着大公司的入场&#xff0c;无疑给创业公司带来了降维打击&#xff0c;创业公司随时可能倒掉&#xff0c;造成项目烂尾。 我也一直在关注大模型领域的最…

【深度学习】yolov 图片训练的时候的遇到的warning: corrupt JPEG restored and saved

报错原因 是图片在dataset.py 走验证时报的错误。 if im.format.lower() in (jpg, jpeg):with open(im_file, rb) as f:f.seek(-2, 2)if f.read() ! b\xff\xd9: # corrupt JPEGImageOps.exif_transpose(Image.open(im_file)).save(im_file, JPEG, subsampling0, quality100)m…

1-8 Burpsuite 漏洞扫描介绍

Burpsuite Scanner介绍 Burp Scanner的功能主要是用来自动检测web系统的各种漏洞&#xff0c;我们可以使用Burp Scanner代替我们手工去对系统进行普通漏洞类型的渗透测试&#xff0c;从而能使得我们把更多的精力放在那些必须要人工去验证的漏洞上。 进一步解放我们的生产力&a…

Spring Boot 中的日志

一、日志有什么用&#xff1f; 日志是程序的重要组成部分&#xff0c;想象一下&#xff0c;如果程序报错了&#xff0c;不让你打开控制台看日志&#xff0c;那么你能找到报错的原因吗&#xff1f; 答案是否定的&#xff0c;写程序不是买彩票&#xff0c;不能完全靠猜&#xf…

腾讯校园招聘技术类编程题汇总

题解&#xff1a;并查集&#xff08;模板&#xff09; #include <iostream> #include<map> using namespace std; int father[2000006]; int rank1[1000005]; void init(int n){for(int i1;i<1e5;i){father[i]i;rank1[i]1;} } int find(int x){if(father[x]x){…

数据可视化 - 动态柱状图

基础柱状图 通过Bar构建基础柱状图 from pyecharts.charts import Bar from pyecharts.options import LabelOpts # 使用Bar构建基础柱状图 bar Bar() # 添加X轴 bar.add_xaxis(["中国", "美国", "英国"]) # 添加Y轴 # 设置数值标签在右侧 b…

上门家政系统开发|上门预约家政小程序定制系统

随着人们生活水平的提高&#xff0c;对于家政服务的需求也越来越高。上门家政小程序的开发为家政服务商家提供了一个全新的经营和服务渠道。本文将介绍上门家政小程序适合的商家以及其优势。   1. 家政公司   家政公司是最直接受益于上门家政小程序开发的商家。通过开发家政…

AMEYA360代理线:ROHM开发出EcoGaN™减少服务器和AC适配器等的损耗和体积!

全球知名半导体制造商ROHM&#xff08;总部位于日本京都市&#xff09;面向数据服务器等工业设备和AC适配器等消费电子设备的一次侧电源*1&#xff0c;开发出集650V GaN HEMT*2和栅极驱动用驱动器等于一体的Power Stage IC“BM3G0xxMUV-LB”&#xff08;BM3G015MUV-LB、BM3G007…

【JavaEE初阶】HTTP协议

文章目录 1. HTTP概述和fiddler的使用1.1 HTTP是什么1.2 抓包工具fiddler的使用1.2.1 注意事项1.2.2 fiddler的使用 2. HTTP协议格式2.1 HTTP请求格式2.1.1 基本格式2.1.2 认识URL2.1.3 方法 2.2 请求报头关键字段2.3 HTTP响应格式2.3.1 基本格式2.3.2状态码 1. HTTP概述和fidd…

nginxWebUI runCmd命令执行漏洞复现

系统描述 NginxWebUI 是一款图形化管理 nginx 配置的工具&#xff0c;可以使用网页来快速配置 nginx单机与集群的各项功能&#xff0c;包括 http协议转发&#xff0c;tcp协议转发&#xff0c;反向代理&#xff0c;负载均衡&#xff0c;静态 html服务器&#xff0c;ssl证书自动申…

【Python机器学习】实验01 Numpy以及可视化回顾

文章目录 一、Numpy的基础知识实验1 生成由随机数组成的三通道图片&#xff0c;分别显示每个维度图片&#xff0c;并将三个通道的像素四周进行填充&#xff0c;分别从上下左右各填充若干数据。 二、Numpy的线性代数运算实验2 请准备一张图片&#xff0c;按照上面的过程进行矩阵…

磁场强度单位和磁感应强度单位转换

磁学量常用单位换算 、磁场强度单位和磁感应强度单位转换。 磁场单位 Oe&#xff08;奥斯特&#xff09;,A/m,T&#xff08;特斯拉&#xff09;三种. 1T1000mT 1mT10Gs 1Gs79.6A/m 1T(特斯拉)10000Gs(高斯)1Wb/M2 1Gs(高斯)1Oe(奥斯特)

C#实现系统进程的调用,查看进程调用的模块

1.需要使用命名空间&#xff1a;System.Diagnostics; 2.Process.GetProcess()可以获取所有进程 3.获取进程调用的模块 ProcessModuleCollection modules currentProcess.Modules; foreach循环一下FileName就可以查看调用了什么dll文件了 4.有关进程的信息&#xff08;Process…

最新基于Citespace、vosviewer、R语言文献计量学可视化分析技术及全流程文献可视化SCI论文高效写作方法

目录 专题一 文献计量学方法与应用简介 专题二 主题确定、检索与数据采集 专题三 VOSviewer可视化绘图精讲 专题四 Citespace可视化绘图精讲 专题五 R语言文献计量学绘图分析 专题六 论文写作 专题七 论文投稿 文献计量学是指用数学和统计学的方法&#xff0c;定量地分析…