将网络应用程序翻译成多种语言是一个常见的要求。在过去,创建多语言应用程序并不是一件容易的事,但最近(感谢Next.js框架和Lingui.js库背后的人们)这项任务变得容易多了。
在这篇文章中,我将向你展示如何用前面提到的工具构建国际化的应用程序。我们将创建一个样本应用程序,它将支持静态渲染和按需语言切换。
设置
首先,我们需要用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/tasks 或es.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更复杂,所以让我们逐一查看每个环节。
locales 和pseudoLocale 分别是我们要生成的所有位置,以及哪些位置将被用作伪位置。
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 │ 1 │ 1 │
│ en │ 1 │ 0 │
│ sr │ 1 │ 1 │
└──────────┴─────────────┴─────────┘
(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 │ 1 │ 0 │
│ en │ 1 │ 0 │
│ sr │ 1 │ 1 │
└──────────┴─────────────┴─────────┘
(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 值为1 ,Plural 组件将呈现 "我们有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博客上。