网站国际化设置
- W_Z_C
- 共 6202 字,阅读约 16 分钟
这次网站重构的重要目的就是支持国际化,本篇文章介绍国际化相关的基础设置。如果想要国际化一个网站,越早介入越好,因为越到后期,修改的成本越高,所以在其它页面还没完善前咱们就先把国际化的大致框架制定好,这样在以后的实现过程中会自然而然的考虑国际化相关的问题,避免不必要的更改成本。
目前想要把网站国际化,有两个地方需要克服:
- 一个是网站页面上的文本信息。
- 一个是以后博客显示的文章详情。
对于网站页面信息来说,需要将页面的文本翻译成不同的语言,并将它们保存在各自的目录中。这里我的做法是,提取现有页面的文字内容,将它们统一放到 json 文件中,页面在显示文本的时候自动从对应语言的 json 文件中提取,进而实现网页的国际化。
对于博客文章来说,在前面的文章中已经提到过,可以直接将 Markdown 文档翻译为指定的语言并保存在本地,当网页需要显示特定语言的文章时,直接渲染对应语言的文本即可。
1. 目录约定
在实现代码之前,我们首先需要制定一个存放多语言文本的规则。前面的文章已经提到过有了一个 i18n 文件夹存放多语言的数据,因此这里直接将以后的多语言文本放入其中,大致的样式如下:
i18n
├─ docs
│ ├─ en
│ └─ zh-Hans
└─ locales
├─ en
│ └─ locale.json
├─ zh-Hans
│ └─ locale.json
└─ zh-Hant
└─ locale.json
其中 i18n/docs
目录存放 Markdown 的翻译文件,而 i18n/locales
存放翻译的网页文本。这种规则并不是定死的,还是根据自身的需要,目前只是觉得这种方案比较适合博客需要,因为除非网站增加额外的菜单或者其它板块内容,否则 i18n/locales
目录下的文件内容很少改动。而 i18n/docs
文件中的内容属于自动生成的性质,即使删除也不会有任何影响,随时可以根据网站根目录的 docs
内容动态生成,因此才会将它们分开。
2. 增加多语言路径
增加多语言内容的第一步是告诉 Nextjs 你有哪些网页,目前 Nextjs 可以使用 getStaticPaths 函数来获取所有需要预渲染的路径,对于多语言网站来说,每种语言都需要设置单独的页面,所以首先需要修改数据库中查找路径的代码:
和原来的代码类似,只不过我们增加一个获取参数:
async function getPostPaths() {
//...
const paths = await db.posts.findMany({ select: { slug: true, i18n: true, } })
return paths}
这样该函数的返回值会带上 i18n 参数,该参数内部保存了当前当前路径上对应的语言,Nextjs 对于多语言路径有特殊的返回方式:
export async function getStaticPaths() {
const meta = await getPostPaths()
const paths = (locales || ['zh-Hans']).map(v => ({ params: { slug: [] }, locale: v }))
.concat(meta.map(v => {
return {
params: { slug: Array.isArray(v.slug) ? v.slug : [v.slug] },
locale: v.i18n,
}
}))
return {
paths,
fallback: false,
}
}
返回路径数组的每一项都包含两个参数,params 用来存放路径信息,locale 用来保存当前的语言区域,这样返回之后,Nextjs 会自动对这些多语言页面进行预处理。
3. Markdown 文档国际化
目前 Markdown 文档全部放在根目录的 docs 中,我们接下来需要将这些文档翻译为国际化需要的语言,推荐手动翻译,不过为了方便这里使用 Google 翻译接口自动批量翻译现有的文档。
3.1 Markdown 文档翻译
Google 翻译的接口具体使用大家可以查询 官方文档,这里只是简单介绍,实现之前先在 backend 目录下建立 translate.mjs 文件,该文件用于实现自动翻译逻辑,然后增加一条执行该文件的命令:
{ "scripts": { "dev": "next dev", "prebuild": "node ./src/backend/script.mjs", "build": "next build", "trans": "node ./src/backend/translate.mjs", "start": "next start", "lint": "next lint" }}
这样可以随时使用 npm run trans
命令翻译现有的 Markdown 文档。
在写代码之前,先让我们安装接下来会用的其它第三方库:
npm install remark remark-parse remark-stringify
npm install vfile
npm install gray-matter
npm install unist-util-visit
npm install @google-cloud/translate
插件比较多,remark 是一个使用插件转换 Markdown 文档的工具。remark-parse 和 remark-stringify 就是这样的插件,前者可以将 Markdown 转换为语法树,后者可以将语法树转回 Markdown 文档。vfile 主要是提供一种虚拟的文件格式,方便 remark 进行文件相关的操作。 gray-matter 前面提到过,用于提取 Markdown 头部的元信息,unist-util-visit 是用来遍历语法树的辅助工具,最后的 @google-cloud/translate 是 Google 官方封装的翻译库。
下面是加载它们的方式:
import { VFile } from 'vfile'
import { TranslationServiceClient } from '@google-cloud/translate'
import { remark } from 'remark'
import matter from 'gray-matter'
import { visit } from 'unist-util-visit'
import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify'
接下来就是如何实现 Markdown 文档翻译的代码,逻辑上很简单,先遍历目录,找出所有需要翻译的 Markdown 文件,然后使用 remark 将文档转换为语法树,接着使用 Google 将语法树中需要翻译的节点进行转换,最后在转换为 Markdown 文档即可。
按照上面的逻辑,先遍历目录,找到所有的文档:
function ThroughDir(root, excludesDir = [], files = []) {
fs.readdirSync(root).forEach(file => {
const abs = path.join(root, file)
const isDir = fs.statSync(abs).isDirectory()
if(isDir && excludesDir.includes(file.toLocaleLowerCase())) {
return
}
if(isDir) {
return ThroughDir(abs, excludesDir, files)
} else {
return files.push(abs)
}
})
return files
}
然后实现翻译函数:
// Instantiates a client
const translationClient = new TranslationServiceClient();
const translateTextAsync = async (str, lang, retry = 3) => {
// Construct request
const request = {
parent: `projects/${projectId}/locations/${location}`,
contents: [str],
mimeType: 'text/plain', // mime types: text/plain, text/html
sourceLanguageCode: 'zh-CN',
targetLanguageCode: lang,
};
let text = str
for(let i=0; i<retry; i++) {
try {
// Run request
const [response] = await translationClient.translateText(request);
text = response.translations[0].translatedText;
break
} catch(e) {
console.log('翻译失败!',str, e.message)
}
}
return text
}
代码中 projectId 和 location 是 Google 翻译 API 需要提供的工程 ID 和接口位置标识,具体可以参考 官方文档。
接来下是使用该函数翻译 Markdown 文档,需要翻译的地方有两处,一处是头部的元信息,包括文章介绍、标签、分类等字段,另一处就是文档的正文。
对于元信息,可以使用下面的函数:
元信息翻译代码
const translateMatter = async (meta) => {
if (!meta) {
return meta
}
let { chapter, category, title, description, tags, slug } = meta
if (chapter) {
let items = chapter.split('_')
chapter = await Promise.all(items.map(v => {
if(isAscii(v))
return Promise.resolve(v)
return translateTextAsync(v, lang, retry)
})).then(arr => arr.join('_'))
}
if (category) {
category = await translateTextAsync(category, lang, retry)
}
if (!isAscii(title)) {
title = await translateTextAsync(title, lang, retry)
}
if (!isAscii(description)) {
description = await translateTextAsync(description, lang, retry)
}
if (tags) {
let items = tags.split('、')
tags = await Promise.all(items.map(v => {
if(isAscii(v))
return Promise.resolve(v)
return translateTextAsync(v, lang, retry)
})).then(arr => arr.join('、'))
}
slug = `${slug}`
let obj = {
...meta,
slug,
title,
description,
}
if (obj.chapter) {
obj.chapter = chapter
}
if (obj.tags) {
obj.tags = tags
}
if (obj.category) {
obj.category = category
}
return obj
}
代码里面引入了一个额外的函数 isAscii,主要用于判断是否为 ASCII 字符串,如果是跳过翻译。
const isAscii = str => /^[\x00-\x7F]+$/.test(str);
翻译正文部分,主要是编写一个 remark 插件,因为 Markdown 文档中并不是所有地方都需要翻译,例如代码部分就不需要,所以这里定义了一个插件,选择某些节点的文本进行翻译,目前实现中我只翻译了 text 和 image 节点,以后可能会有所改动,看以后翻译的质量再定。
const remarkTranslate = () => {
return async (tree) => {
const tasks = []
visit(tree, (node) => {
if (node.type === 'text' && (node.value || '').trim().length > 0 && (!isAscii(node.value))) {
tasks.push(translateTextAsync(node.value, lang, retry).then((text) => {
node.value = text
}))
} else if (node.type === 'image' && (node.alt || '').trim().length > 0 && (!isAscii(node.alt))) {
tasks.push(translateTextAsync(node.alt, lang, retry).then((text) => {
node.alt = text
}))
}
})
await Promise.all(tasks)
}
}
因为 Google 提供的翻译接口是异步的,所以只能使用 Promise.all 这样的方式,不知道是否能够保证翻译正确,目前从结果上看还正常,如果以后发现异常再说。
最后,就是整合这些函数,翻译最终的 Markdown 文档:
翻译函数代码
async function translateFilesAsync(root, files, lang) {
//建立文件夹
const dir = path.join(root, lang)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, {recursive: true})
}
//读取文件
for (let i = 0; i < files.length; i++) {
console.log(i + '/' + files.length)
const file = matter.read(files[i])
//如果是草稿,跳过
if (file.data.isDraft) {
continue
}
//目标文件存在,跳过
const filepath = path.join(dir, `${file.data.slug}.md`)
if (fs.existsSync(filepath)) {
const transFile = matter.read(filepath)
if (transFile.data.updatedAt && file.data.updatedAt &&
transFile.data.updatedAt.getTime() === file.data.updatedAt.getTime()) {
console.log(files[i], '文件已经生成过了')
continue
}
}
const mdContent = await remark()
.use(remarkParse)
.use(remarkTranslate)
.use(remarkStringify)
.process(new VFile(file.content));
const metaContent = await translateMatter(file.data);
const fileContent = matter.stringify(mdContent.value, metaContent);
fs.writeFileSync(filepath, fileContent);
}
}
因为在 package.json 中执行的是 translate.mjs 文件,所以还要把执行逻辑放到最后:
(async() => {
const files = ThroughDir('./docs', [])
await translateFilesAsync('./i18n/docs', files, 'zh-Hant')
})()
上面就是所有的翻译逻辑了,但是如果你直接执行脚本,可能会产生下面的错误:
Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
报错的原因主要是接口授权导致的,你可以点击提示的链接查看相关信息,大致需要你先下载 Google 提供的授权证书,然后设置 GOOGLE_APPLICATION_CREDENTIALS 环境变量为证书所在的路径才可以。
最后在终端执行翻译命令:
npm run trans
你可以在 docs/i18n/zh-Hant
文件夹中查看最终翻译的结果。
3.2 数据库保存
Markdown 除了翻译之外,还需要将文档内容同步保存到数据库。在前面的章节中,我们已经实现了 Markdown 保存到数据库的逻辑,接下来咱们只需要修改一些代码,确保将翻译后的文档也添加进去即可。
打开 backend 目录下的 init.mjs
文件,修改 getMDXList 函数:
async function getMDXList() { const orginDoc = await getPathAll(DOCROOT) const i18nDoc = await getPathAll(DOCI18NROOT, {}, {i18n: true}) const files = orginDoc.concat(i18nDoc)
let posts = [] let onces = []
for (let i = 0; i < files.length; i++) {
//...
//如果是翻译文本 if(files[i].i18n) { const items = (files[i].dir || '').split(path.sep) post.i18n = items.length > 0 ? items[items.length -1] : '' }
if (!onces[post.slug]) { onces[post.slug] = true } else { console.error(post.filename, 'URL 重复!') }
posts.push(post) }
return posts}
代码额外读取了 i18n 文件夹的内容,然后在每篇文章对象上追加了一个 i18n 的属性,属性值的内容是文档所处的目录名称,因为我们规定文档所在的目录恰好是当前语言,也就是说,i18n 的属性值为当前文档的语言,默认为简体中文(zh-Hans)。
最后执行 npm run prebuild
命令将翻译后的数据插入到数据库。
4. 网页文本国际化
4.1 静态文本替换
页面国际化的第一步可以先抽取网页中需要翻译的文本,放到 locale.json 文件中,下面是目前主页需要翻译的文本内容:
i18n/zh-Hans/locale.json 文件
{
"Header": {
"Title": "没事造轮子",
"OpenMenu": "打开菜单",
"CloseMenu": "关闭菜单",
"Register": "注册",
"Login": "登陆",
"LoginQ": "已经注册?",
"PCMenu": [{
"ID": "Tutorials",
"Title": "教程",
"Description": "系统化讲解,以点带面,引导用户理解相关知识内容。",
"Items": [{
"Name": "Win32 入门教程",
"Description": "Windows 操作系统的核心操作接口,独领风骚二十年。",
"Link": "windows-bian-cheng-kai-fa"
}, {
"Name": "SDL2 游戏开发教程",
"Description": "开源的多媒体跨平台开发库,纯 C 接口,易于上手。",
"Link": "sdl2-you-xi-kai-fa-zui-xiao-zhi-shi-zhan"
}, {
"Name": "HTML 入门教程",
"Description": "超文本标记语言,前端网页的骨架,强调语义化。",
"Link": "html-jian-jie"
}]
}, {
"ID": "Projects",
"Title": "项目",
"Description": "遵守最小化知识栈原则,以实战为目的多媒体教程。",
"Groups": [{
"Name": "游戏开发",
"Projects": [{
"Title": "俄罗斯方块",
"Image": "/assert/images/menu-project-tetris.gif",
"Link": "cyu-yan-xiao-you-xi-zhi-e-luo-si-fang-kuai"
}, {
"Title": "方块赛车",
"Image": "/assert/images/menu-project-sz.gif",
"Link": "windows-you-xi-bian-cheng-zhi-fang-kuai-sai-che"
}, {
"Title": "打砖块",
"Image": "/assert/images/menu-project-breakout.gif",
"Link": "windows-you-xi-bian-cheng-zhi-da-zhuan-kuai"
}, {
"Title": "扫雷",
"Image": "/assert/images/menu-project-mines.jpg",
"Link": "windows-you-xi-bian-cheng-zhi-sao-lei"
}]
}, {
"Name": "网站开发",
"Projects": [{
"Title": "Nextjs 博客搭建教程",
"Image": "/assert/images/menu-project-nextjs.jpg",
"Link": "next-blog-tutorials"
}]
}]
}, {
"ID": "ReadCodes",
"Title": "码读",
"Description": "源码阅读心得",
"Items": [{
"Name": "Express 源码刨析",
"Link": "express"
}]
}, {
"ID": "More",
"Title": "更多",
"Description": "",
"Items": [{
"Name": "下载",
"Description": "博客文章中提到的一些第三方资源、演示程序等内容。",
"Link": "download"
}, {
"Name": "关于",
"Description": "关于博主、博客的一些基本信息。",
"Link": "about"
}]
}]
},
"Hero": {
"Title": "学习、实战、总结、",
"SubTitle": "分享",
"Action": "开始学习"
},
"TutorialList": {
"Title": "技术教程",
"Description": "专注于某项技术的系统性课程,从零开始,深入简出。",
"Items": [{
"Title": "Win32",
"Description":"Windows 操作系统的核心操作接口,独领风骚二十年。",
"Action": "开始学习",
"Link": "windows-bian-cheng-kai-fa"
}, {
"Title": "SDL2",
"Description":"开源的多媒体跨平台开发库,纯 C 接口,易于上手。",
"Action": "开始学习",
"Link": "sdl2-you-xi-kai-fa-zui-xiao-zhi-shi-zhan"
}, {
"Title": "CSS",
"Description":"前端学习的必备知识,控制网站样式的专用语言。",
"Action": "敬请期待",
"Link": "#",
"UnPublished": true
}]
},
"RecentPosts": {
"Title": "最新文章",
"Loading": "加载中……"
},
"Footer": {
"Title": "没事造轮子"
}
}
接着将该文档的内容翻译为对应的语言放到各自的目录中。接下来需要解决的问题是如何在对应的位置替换对应的语言文字。为了解决这个问题,我们可以定义一个函数,获取对应语言的文本内容:
import * as path from 'path'
import * as fs from 'fs'
let cacheLocales = {}
function loadCache(locale) {
try {
cacheLocales[locale] = JSON.parse(fs.readFileSync(locale))
} catch(e) {
console.error('加载文件失败!', locale, e.message)
}
}
export function getLocale(locale = 'zh-Hans') {
let localeDirectory = path.join(process.cwd(), 'i18n/locales', locale, 'locale.json')
if(!fs.existsSync(localeDirectory)) {
localeDirectory = path.join(process.cwd(), 'i18n/locales/zh-Hans/locale.json')
}
//查询缓存,尝试加载
if(!cacheLocales[localeDirectory]) {
loadCache(localeDirectory)
}
return cacheLocales[localeDirectory] || {}
}
getLocale 负责获取对应语言的文本集,之所以获取整个文件内容而不是获取更细粒度的某条文本,主要的原因是 Nextjs 的机制决定的。前面的文章提到过,Nextjs 渲染页面常见的两种模式,一种是静态生成(SSG,Static Site Generation),另一种是服务器渲染(SSR,Server-Side Rendering)。
我们这里会尽量使用静态生成的方式,对于多语言页面来说,会根据不同的 locale 生成不同的页面,而静态生成页面会在编译器间运行,所以不管使用哪种文本读取的方式,都不会对网站运行的效率产生任何影响,再加上读取完整的文本内容在代码处理上更加容易,所以才会选择这样的实现方式。当然为了提高编译效率,函数内部还是做了缓存的。
按照 Nextjs 官方文档的指引,可以修改现有的 getStaticProps 函数代码:
export async function getStaticProps({params, locale, defaultLocale}) {
//...
}
参数 locale 和 defaultLocale 分别代表当前页面的语言区域和默认的语言区域,前者的值是 Nextjs 自动检测出来的,后者是在 next.config.js 中设置的。两个参数是 Nextjs 自动传递的,不过要想它们生效,需要更改 Nextjs 的配置文件,增加国际化路由的相关设置。
i18n: {
locales: ['zh-Hans', 'zh-Hant', 'en'],
defaultLocale: 'zh-Hans',
localeDetection: false
},
其中 localeDetection 表示是否支持语言的自动检测功能。如果为真, Nextjs 会尝试根据 Accept-Language 和当前区域自动推测用户的使用语言,如果检测到和默认语言不相符,则会重定向到特定语言页面。当使用子路由区分语言时,会自动增加语言前缀;如果使用的是域路由,则会跳转到对应的域名,为了防止误判,这里选择关闭该功能。
设置 i18n 相关配置后,getStaticProps 函数就可以正常接收到国际化相关的参数。该函数返回的是一个对象,props 属性的值可以传递给函数组件,你可以直接在函数组件内使用这些对象,前面的章节中已经使用过了,现在需要增加一个额外的 trans 属性,用来存放翻译的文本内容:
export async function getStaticProps({params, locale, defaultLocale}) {
//...
//获取翻译文本
const loc = locale || defaultLocale
const trans = getLocale(loc)
//寻找对应的模板,获取模板属性
const {layout = type, payload = {}} = await getProps(type, {slug, locale: loc})
return {
props: {
layout,
locale: loc,
trans,
payload,
}
}
}
代码中主要做了两件事,一个是通过 getLocale 获取当前区域对应的文本内容,另一个是将 locale 参数传入到 getProps 函数,便于不同区域文章的内容显示。
之所以直接在 getStaticProps 获取翻译文件,而不是在组件中获取的原因在于每个页面都会用到翻译文本。毕竟目前所有页面都会包含 Header 和 Footer 两个组件,而将 getLocale 放到这里可以避免单一组件重复调用 getLocale 函数的尴尬。
这里有一点需要注意,在返回参数的时候,我们也将当前的语言 locale 参数提供给了组件,一般情况下组件是用不到这个参数的,但是有一种情况下你可能会用到它。那就是当需要调用后台 API 接口的时候,因为后台 API 接口是单独存在的,无法获知当前你所请求的语言类型,所以你需要告知它,这里将 locale 参数提供给组件,到时候在组件内部就可以直接将该参数传递给后台,后台有了这个参数才能区分当前接口需要返回的语言类型。
接下来是将翻译文本投递到各个页面布局组件中:
export default function Page({layout, locale, trans, payload}) { const Layout = getLayout(layout) return ( <Layout trans={trans} locale={locale} {...payload}/> )}
这里用主页布局当作例子:
export function HomeLayout({locale, trans, posts }) {
return (
<>
<Header
locale={locale}
i18n={{
...trans.Header
}}
/>
<Hero
locale={locale}
i18n={{
...trans.Hero
}}
/>
<TutorialList
locale={locale}
i18n={{
...trans.TutorialList
}}
/>
<RecentPosts
locale={locale}
i18n={{
...trans.RecentPosts,
}}
posts={posts}
/>
<Footer
locale={locale}
i18n={{
...trans.Footer
}}
/>
</>
)
}
所有的组件都增加两个参数,接着需要修改每个组件的原有文本内容,这里拿 Hero 组件作为案例,其它的代码类似:
export function Hero({locale, i18n}) { return ( <div className="py-20 md:py-32"> <section className="container md:flex mx-auto"> ...
<div className="w-full mt-10 md:w-1/2"> <h2 className="text-6xl text-center leading-normal md:leading-relaxed md:text-[4rem] lg:text-[5rem]"> <span>{i18n.Title}</span> <span className="relative inline-block overflow-visible"> <span className="font-black z-10 relative">{i18n.SubTitle}</span> <svg className="text-primary absolute -translate-y-1/2 -translate-x-1/2 top-1/2 left-1/2 h-[calc(100%)] w-[calc(100%+2rem)]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 150" preserveAspectRatio="none"><path fill="none" stroke="currentColor" strokeWidth="9" d="M7.7,145.6C109,125,299.9,116.2,401,121.3c42.1,2.2,87.6,11.8,87.3,25.7"></path></svg> </span> </h2>
<div className="text-center mt-16"> <Link href="#tutorial-hero" passHref> <a className="inline-block py-4 px-14 rounded-md bg-primary transform duration-500 hover:scale-110 motion-reduce:transform-none"> <span className="flex text-xl text-white font-bold"> <svg className="w-4 mr-2 fill-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M381.2 172.8C377.1 164.9 368.9 160 360 160h-156.6l50.84-127.1c2.969-7.375 2.062-15.78-2.406-22.38S239.1 0 232 0h-176C43.97 0 33.81 8.906 32.22 20.84l-32 240C-.7179 267.7 1.376 274.6 5.938 279.8C10.5 285 17.09 288 24 288h146.3l-41.78 194.1c-2.406 11.22 3.469 22.56 14 27.09C145.6 511.4 148.8 512 152 512c7.719 0 15.22-3.75 19.81-10.44l208-304C384.8 190.2 385.4 180.7 381.2 172.8z"/></svg> <span>{i18n.Action}</span> </span> </a> </Link> </div> </div> </section> </div> )}
按照上面的方法依次类推,可以将所有组件的文本全部替换完毕,这样网页文本的国际化基本上算是初步搞定。
4.2 动态文本替换
除了组件中静态文本的替换外,其实还存在动态文本的替换问题。目前博客有两个模块会涉及到这个内容,一个是首页的最新文章组件,因为该模块会显示最近几篇 Markdown 文档的元信息,例如标题、简介等内容,这样的内容肯定会被翻译为多国语言,所以你需要按照当前的区域显示不同的 Markdown 的翻译文本。另一个地方就是文章显示页面,这个不用多说,就是根据当前语言区域显示对应的 Markdown 文档内容。
如何实现呢?实现的方式非常简单,因为这些内容都是动态加载的,所以只需要重构读取数据的逻辑,在查询数据的过程中增加额外的语言区域参数,这样在查找文档的时候,就可以查询到对应语言的文档内容,这样最后显示页面自然而然的就支持多语言的动态切换。
在前面的内容中,已经提到过,数据的加载在 getStaticProps 函数中完成:
export async function getStaticProps({params, locale, defaultLocale}) {
//...
//寻找对应的模板,获取模板属性 const {layout = type, payload = {}} = await getProps(type, {slug, locale: locale || defaultLocale})
//...}
所以我们可以将 locale 参数的内容透传到 getProps 函数,然后在查询数据的时候,增加额外的 locale 参数,这样查询的结果自然会匹配对应的语言。
例如,对于普通的文章页面,修改查询参数:
export async function postProps({slug, locale}) {
const post = await getPostBySlug(slug, locale)
//...
}
接着修改数据库的查询条件:
async function getPostBySlug(slug, locale) {
//...
const post = await db.posts.findFirst({
where: {
AND: [{
slug: { equals: slug }
}, {
i18n: { equals: locale }
}]
},
//...
})
//...
}
这样查询的结果自然会返回对应的语言。首页的最新文章模块修改和上面类似,但是因为涉及到 API 接口的访问,需要额外传递语言参数来进行区分,所以更加麻烦一点。
该模块的数据查询可以更改为:
export async function homeProps({locale}) { const posts = await getRecentPosts(locale)
return { payload: { posts, } }}
然后在 getRecentPosts 函数中增加 locale 查询条件:
async function getRecentPosts(locale, start = 0) {
//...
const metas = await db.posts.findMany({
where: {
AND: [{
category: {in: ['博客', '编译', '研究']},
}, {
i18n: { equals: locale }
}]
},
//...
})
//...
}
不要忘记除了上面提到的代码之外,还有一个地方调用了 getRecentPosts 函数,那里一样需要传入 locale 参数,否则肯定会报错。
遗憾的是在后台 API 接口的函数中,我们暂时无法获取到当前的语言环境,解决这个问题目前我能想到两种方式,一种是使用 cookie,这样所有的后台请求都会自动添加语言参数,另一种前文已经提到了,就是传入前台组件的时候,增加一个额外的变量,用来保存当前使用的语言,每次请求后台 API 接口的时候,将该参数一起传入即可。目前 getStaticProps 函数的返回值已经增加了该变量(locale)。
为了将该变量从前台传递到后台 API 接口,我们需要修改 RecentPosts 组件代码:
export function RecentPosts({ locale, i18n, posts }) {
const [recentPosts, setRecentPosts] = useState({ posts: posts, hasMore: true })
const fetchMoreData = () => { fetch('/api/recentposts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ locale: locale, count: recentPosts.posts.length || 0, }) })
//... }
//...}
后台 API 接口同样需要接收 locale 参数:
export default function RecentPosts(req, res) {
if(req.method !== 'POST') { return res.status(200).json({ success: true, posts: [] }) } else { const submitSchema = Joi.object({ count: Joi.number().integer().positive(), locale: Joi.string().max(10).empty('').default('zh-Hans'), })
const {err, value} = submitSchema.validate(req.body) if(err) { return res.status(200).json({ success: false, message: err.message || '', }) }
return getRecentPosts(value.locale, value.count) .then(function(posts) { //... }) }}
代码中增加了简单的语言校验,然后将 locale 参数传递给前面修改后的 getRecentPosts 函数。
5. 总结
到此为止,算是把网站国际化涉及的内容初步讲解完毕,核心内容在于如何使用 locale 参数,对于页面中导出函数可以直接从参数中获取该值,但是如果使用的是后台 API 接口,那么你需要将当前使用的语言告知该接口,这样接口才能有对该语言特殊处理的可能,否则接口是无法获知当前需要返回哪种语言的。