用Next.js实现国际化的完整指南

5,258 阅读11分钟

将网络应用程序翻译成多种语言是一个常见的要求。在过去,创建多语言应用程序并不是一件容易的事,但最近(感谢Next.js框架和Lingui.js库背后的人们)这项任务变得容易多了。

在这篇文章中,我将向你展示如何用前面提到的工具构建国际化的应用程序。我们将创建一个样本应用程序,它将支持静态渲染和按需语言切换。

Gif of sample Next.js application switching between languages

你可以查看这个演示,并在这里分叉存储库

设置

首先,我们需要用TypeScript创建Next.js应用程序。在终端中输入以下内容。

npx create-next-app --ts

接下来,我们需要安装所有需要的模块。

npm install --save-dev @lingui/cli @lingui/loader @lingui/macro babel-plugin-macros @babel/core
npm install --save @lingui/react make-plural

Next.js中的国际化路由

Next.js应用程序国际化的一个基本方面是国际化的路由功能,因此具有不同语言偏好的用户可以登陆不同的页面,并能够链接到它们。

此外,通过在网站头部设置适当的链接标签,你可以告诉谷歌在哪里可以找到所有其他语言版本的页面,以便正确索引。

Next.js支持两种类型的国际化路由方案。

首先是子路径路由,其中第一个子路径(www.myapp.com/{language}/… [http://www.myapp.com/en/tasks](http://www.myapp.com/en/tasks)[http://www.myapp.com/es/tasks](http://www.myapp.com/es/tasks).在第一个例子中,用户将使用英语版本的应用程序(en),在第二个例子中,用户将使用西班牙语版本(es)。

第二种是域路由。通过域路由,你可以为同一个应用程序设置多个域,每个域将提供不同的语言服务。例如,en.myapp.com/taskses.myapp.com/tasks

Next.js如何检测用户的语言

当用户访问应用程序的根或索引页面时,Next.js将尝试根据Accept-Language header自动检测用户喜欢哪个位置。如果该语言的位置被设置了(通过Next.js的配置文件),用户将被重定向到该路线。

如果不支持该位置,用户将被提供默认的语言路线。该框架还可以使用cookie来确定用户的语言。

如果用户的浏览器中存在NEXT_LOCALE cookie,框架将使用该值来决定向用户提供哪种语言路由,而Accept-Language 头部将被忽略。

配置我们的Next.js应用样本

我们的演示将有三种语言:默认的英语(en ),西班牙语(es ),以及我的母语塞尔维亚语(sr )。

因为默认语言是英语,任何其他不支持的语言都将默认为英语。

我们还将使用subpath 路由来传递页面,像这样。

//next.config.js

module.exports = {
  i18n: {
    locales: ['en', 'sr', 'es', 'pseudo'],
    defaultLocale: 'en'
  }
}

在这个代码块中,locales 是我们想要支持的所有语言,defaultLocale 是默认语言。

你会注意到,在配置中,还有第四种语言:pseudo 。我们将在后面讨论更多这方面的内容。

正如你所看到的,这个Next.js的配置很简单,因为该框架只用于路由,没有其他功能。你将如何翻译你的应用程序取决于你。

配置Lingui.js

对于实际的翻译,我们将使用Lingui.js

让我们来设置一下配置文件。

// lingui.config.js

module.exports = {
  locales: ['en', 'sr', 'es', 'pseudo'],
  pseudoLocale: 'pseudo',
  sourceLocale: 'en',
  fallbackLocales: {
    default: 'en'
  },
  catalogs: [
    {
      path: 'src/translations/locales/{locale}/messages',
      include: ['src/pages', 'src/components']
    }
  ],
  format: 'po'
}

Lingui.js的配置比Next.js更复杂,所以让我们逐一查看每个环节。

localespseudoLocale 分别是我们要生成的所有位置,以及哪些位置将被用作伪位置。

sourceLocale 后面是en ,因为在生成翻译文件时,默认字符串将是英文的。这意味着,如果你不翻译某个字符串,它将被留在默认的,或源语言中。

fallbackLocales 属性与Next.js的默认区域设置无关,它只是意味着如果你试图加载一个不存在的语言文件,Lingui.js将退回到default 语言(在我们的例子中,是英语)。

catalogs:path 是生成的文件的保存路径。catalogs:include 指示Lingui.js去哪里寻找需要翻译的文件。在我们的例子中,这就是src/pages 目录,以及所有位于src/components 的React组件。

format 是生成文件的格式。我们使用的是po ,这是推荐的格式,但也有其他格式,比如json

Lingui.js如何与React一起工作

我们有两种方法可以将Lingui.js与React一起使用。我们可以使用库中提供的常规React组件,或者使用同样由库提供的Babel宏。

Linqui.js有特殊的React组件和Babel宏。宏在Babel处理你的代码之前对其进行转换,以生成最终的JavaScript代码。

如果你想知道这两者之间的区别,请看一下这些例子。

//Macro
import { Trans } from '@lingui/macro'

function Hello({ name }: { name: string }) {
  return <Trans>Hello {name}</Trans>
}


//Regular React component
import { Trans } from '@lingui/react'

function Hello({ name }: { name: string }) {
  return <Trans id="Hello {name}" values={{ name }} />
}

你可以看到,宏和生成的React组件之间的代码非常相似。宏使我们能够省略id 属性,写出更简洁的组件。

现在让我们为其中一个组件设置翻译。

// src/components/AboutText.jsx

import { Trans } from '@lingui/macro'

function AboutText() {
  return (
    <p>
      <Trans id="next-explanation">My text to be translated</Trans>
    </p>
  )
}

在我们完成组件后,下一步是将需要翻译的源代码中的文本提取到外部文件中,称为消息目录。

消息目录是你要给你的翻译人员进行翻译的文件。每种语言都会有一个文件生成。

为了提取所有的消息,我们要通过命令行使用Lingui.js并运行。

npm run lingui extract

输出结果应该如下。

Catalog statistics:
┌──────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├──────────┼─────────────┼─────────┤
│ es       │      11    │
│ en       │      10    │
│ sr       │      11    │
└──────────┴─────────────┴─────────┘

(use "lingui extract" to update catalogs with new messages)
(use "lingui compile" to compile catalogs for production)

Total count 是需要翻译的信息总数,在我们的代码中,我们只有一条来自AboutText.jsx (ID:next-explanation )的信息。

Missing 是需要翻译的消息的数量。因为英语是默认语言,所以en 版本没有缺失消息。然而,我们缺少塞尔维亚语和西班牙语的翻译。

en 生成的文件的内容将是这样的。

#: src/components/AboutText.jsx:5
msgid "next-explanation"
msgstr "My text to be translated"

es 文件的内容将是如下。

#: src/components/AboutText.jsx:5
msgid "next-explanation"
msgstr ""

你会注意到,msgstr 是空的。这就是我们需要添加我们的翻译的地方。如果我们让这个字段为空,在运行时,所有引用这个msgid 的组件都将用默认语言文件中的字符串填充。

让我们翻译一下西班牙语文件。

#: src/components/AboutText.jsx:5
msgid "next-explanation"
msgstr "Mi texto para ser traducido"

现在,如果我们再次运行extract 命令,这将是输出结果。

Catalog statistics:
┌──────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├──────────┼─────────────┼─────────┤
│ es       │      10    │
│ en       │      10    │
│ sr       │      11    │
└──────────┴─────────────┴─────────┘

(use "lingui extract" to update catalogs with new messages)
(use "lingui compile" to compile catalogs for production)

注意西班牙语的Missing 字段是0 ,这意味着我们已经翻译了西班牙语文件中所有缺失的字符串。

这就是翻译的要点,现在让我们开始将Lingui.js与Next.js进行整合。

编译信息

为了让应用程序能够使用带有翻译的文件(.po ),它们需要被编译成JavaScript。为此,我们需要使用lingui compile CLI命令。

命令运行完毕后,你会注意到在locale/translations 目录中,每个地区都有新的文件(es.js,en.js, 和sr.js )。

├── en
│   ├── messages.js
│   └── messages.po
├── es
│   ├── messages.js
│   └── messages.po
└── sr
    ├── messages.js
    └── messages.po

这些文件将被加载到应用程序中。将这些文件视为构建工件,不要用源码控制来管理它们;只有.po 文件应该被添加到源码控制。

使用复数的工作

还有一件事肯定会出现,那就是处理单数或复数的单词(在演示中,你可以用Developer下拉元素来测试)。

Lingui.js让这个问题变得非常简单。

import { Plural } from '@lingui/macro'

function Developers({ developerCount }) {
  return (
    <p>
      <Plural
        value={developerCount}
        one="Whe have # Developer"
        other="We have # Developers"
      />
    </p>
  )
}

developerCount 值为1Plural 组件将呈现 "我们有1个开发人员"。

你可以在Lingui.js的文档中阅读更多关于复数的内容。

现在,不同的语言有不同的复数化规则。为了适应这些规则,我们以后将使用一个额外的包,叫做 make-plural.

Next.js和Lingui.js的集成

现在最难的部分来了:将Lingui.js与Next.js框架整合。

首先,我们要初始化Lingui.js。

// utils.ts

import type { I18n } from '@lingui/core'
import { en, es, sr } from 'make-plural/plurals'

//anounce which locales we are going to use and connect them to approprite plural rules
export function initTranslation(i18n: I18n): void {
  i18n.loadLocaleData({
    en: { plurals: en },
    sr: { plurals: sr },
    es: { plurals: es },
    pseudo: { plurals: en }
  })
}

因为初始化应该只对整个应用程序做一次,我们将从Next.js [_app](https://nextjs.org/docs/advanced-features/custom-app)组件中调用该函数,该组件在设计上封装了所有其他组件。

// _app.tsx

import { i18n } from '@lingui/core'
import { initTranslation } from '../utils'

//initialization function
initTranslation(i18n)

function MyApp({ Component, pageProps }) {
  // code ommited
}

在Lingui.js代码被初始化后,我们需要加载并激活相应的语言。

同样,我们将使用_app ,像这样。

// _app.tsx

function MyApp({ Component, pageProps }) {
  const router = useRouter()
  const locale = router.locale || router.defaultLocale
  const firstRender = useRef(true)

  if (pageProps.translation && firstRender.current) {
    //load the translations for the locale
    i18n.load(locale, pageProps.translation)
    i18n.activate(locale)
    // render only once
    firstRender.current = false
  }

  return (
    <I18nProvider i18n={i18n}>
      <Component {...pageProps} />
    </I18nProvider>
  )
}

所有消耗翻译的组件都需要在Lingui.js<I18Provider> 组件下。为了确定加载哪种语言,我们要查看Next.js路由器的locale 属性。

翻译是通过pageProps.translation 传递给组件的。如果你想知道pageProps.translation 属性是如何创建的,我们接下来要解决这个问题。

src/pages 中的每一个页面在被渲染之前都需要加载相应的翻译文件,这些文件位于src/translations/locales/{locale}

因为我们的页面是静态生成的,我们将通过Next.jsgetStatisProps 函数来完成。

// src/pages/index.tsx

export const getStaticProps: GetStaticProps = async (ctx) => {
  const translation = await loadTranslation(
    ctx.locale!,
    process.env.NODE_ENV === 'production'
  )

  return {
    props: {
      translation
    }
  }
}

正如你所看到的,我们正在用loadTranslation 函数加载翻译文件。这是它的外观。

// src/utils.ts

async function loadTranslation(locale: string, isProduction = true) {
  let data
  if (isProduction) {
    data = await import(`./translations/locales/${locale}/messages`)
  } else {
    data = await import(
      `@lingui/loader!./translations/locales/${locale}/messages.po`
    )
  }

  return data.messages
}

这个函数的有趣之处在于,它有条件地加载文件,这取决于我们是否在生产中运行Next.js项目。

这是Lingui.js的一个优点;当我们在生产中时,我们要加载已编译的(.js)文件,但当我们在开发模式中时,我们要加载源文件(.po)。只要我们改变了.po 文件中的代码,它就会立即反映在我们的应用程序中。

请记住,.po 文件是我们编写翻译的源文件,然后将其编译为普通的.js 文件,并在生产中用常规的JavaScriptimport 语句加载。如果不是因为有特殊的 @lingui/loader!webpack插件,我们将不得不不断地手动编译翻译文件,以便在开发时看到变化。

动态地改变语言

到此为止,我们处理了静态生成,但我们也希望能够在运行时通过下拉菜单动态地改变语言。

首先,我们需要修改_app 组件,以观察位置变化,并在router.locale 值变化时开始加载适当的翻译。这是非常简单的;我们需要做的就是使用useEffect 钩子。

这里是最终的_app 组件。

// _app.tsx
// import statements omitted

initTranslation(i18n)

function MyApp({ Component, pageProps }) {
  const router = useRouter()
  const locale = router.locale || router.defaultLocale
  const firstRender = useRef(true)

  // run only once on the first render (for server side)
  if (pageProps.translation && firstRender.current) {
    i18n.load(locale, pageProps.translation)
    i18n.activate(locale)
    firstRender.current = false
  }

  // listen for the locale changes
  useEffect(() => {
    if (pageProps.translation) {
      i18n.load(locale, pageProps.translation)
      i18n.activate(locale)
    }
  }, [locale, pageProps.translation])

  return (
    <I18nProvider i18n={i18n}>
      <Component {...pageProps} />
    </I18nProvider>
  )
}

接下来,我们需要建立下拉组件。每当用户从下拉菜单中选择不同的语言时,我们将加载相应的页面。

为此,我们将使用Next.js的 [router.push](https://nextjs.org/docs/api-reference/next/router#routerpush)方法来指示Next.js改变页面的语言环境(这又会被我们在_app 组件中创建的useEffect )。

// src/components/Switcher.tsx

import { useRouter } from 'next/router'
import { useState, useEffect } from 'react'
import { t } from '@lingui/macro'

type LOCALES = 'en' | 'sr' | 'es' | 'pseudo'

export function Switcher() {
  const router = useRouter()
  const [locale, setLocale] = useState<LOCALES>(
    router.locale!.split('-')[0] as LOCALES
  )

  const languages: { [key: string]: string } = {
    en: t`English`,
    sr: t`Serbian`,
    es: t`Spanish`
  }

  // enable 'pseudo' locale only for development environment
  if (process.env.NEXT_PUBLIC_NODE_ENV !== 'production') {
    languages['pseudo'] = t`Pseudo`
  }

  useEffect(() => {
    router.push(router.pathname, router.pathname, { locale })
  }, [locale, router])

  return (
    <select
      value={locale}
      onChange={(evt) => setLocale(evt.target.value as LOCALES)}
    >
      {Object.keys(languages).map((locale) => {
        return (
          <option value={locale} key={locale}>
            {languages[locale as unknown as LOCALES]}
          </option>
        )
      })}
    </select>
  )
}

伪本地化

现在我将解决你在例子中看到的所有pseudo 代码。

伪本地化是一种软件测试方法,它用改变过的版本替换文本字符串,同时仍然保持字符串的可见性。这样就很容易发现我们在Lingui.js组件或宏中漏掉了哪些字符串的包装。

因此,当用户切换到pseudo ,应用程序中的所有文本都应该像这样被修改。

Account Settings --> [!!! Àççôûñţ Šéţţîñĝš !!!]

如果有任何文本没有被修改,那就意味着我们可能忘记做了。当涉及到Next.js时,该框架对特殊的pseudo 本地化没有任何概念,它只是另一种要被路由到的语言。然而,Lingui.js需要特殊的配置。

除此之外,pseudo 只是我们可以切换到的另一种语言。pseudo 本地化应该只在development 模式下启用。

总结

在这篇文章中,我向你展示了如何对Next.js应用程序进行翻译和国际化。我们已经完成了多语言的静态渲染和按需的语言切换。我们还创建了一个很好的开发工作流程,在这个流程中,我们不需要在每次修改时都手动编译翻译字符串。接下来,我们实现了一个pseudo locale,以便直观地检查是否有遗漏的翻译。

如果你有任何问题,请在评论中发表,或者如果你发现演示中的代码有任何问题,请务必在github资源库中打开一个问题。

Next.js中的国际化完整指南》一文首次出现在LogRocket博客上。