在全球化的背景下,越来越多的应用需要支持多语言用户。实现这种多语言支持的过程通常被称为国际化(i18n)。在 React 项目中,react-i18next
是一个广泛使用的库,它为开发者提供了强大的工具来简化国际化的实现。
常用的国际化方案
目前,国际化的实现通常有以下几种方案:
-
静态语言包:在项目中预先定义好多语言的静态文件,例如 JSON 文件,然后根据用户的语言选择加载相应的文件。这种方法简单直接,但在需要频繁更新翻译内容时不够灵活。
-
动态加载语言包:根据用户的语言设置,动态从服务器加载语言包。这种方法可以减少初始加载时间,并且更容易管理和更新。
-
第三方服务集成:使用第三方国际化服务(如 Google 翻译 API)来实时翻译内容。这种方法适合需要快速支持多语言的场景,但可能存在翻译质量不稳定的问题。
尽管这些方法各有优劣,但在实际应用中,我们常常需要一个更灵活且可管理的方案。这就引出了我们开发的重点:如何在 React 项目中高效管理和使用语言包,特别是当语言包需要从远程服务器获取时。
在接下来的内容中,我们将分享一种结合本地和远程管理语言包的方法,不仅提高了国际化工作的效率,还确保了开发和生产环境中的灵活性和稳定性。
在本文中,我们将探讨如何在 React 项目中使用 i18n,自动化上传语言包,从服务器获取语言包,同时如果是本地启动的话优先使用本地语言包数据,以确保开发和测试的灵活性,以及接口报错兼容和本地语言包的自动化更新。
1. 安装和配置 i18n 插件
首先,安装 react-i18next
插件:
1npm install i18next react-i18next
我们还推荐使用 VSCode 插件 du-i18n
,该插件提供了丰富的 i18n 支持和工具。安装后,可以通过扩展面板启用,并按照文档中的指南进行配置,包括一键生成对应的key和内容,以及对应的语言翻译,不过翻译的话有数量限制,但是总体的功能还是十分强大的,目前试了一下应该是国际化里面最好用的扩展了。
2. 怎么上传语言包到服务器
简单的流程图展示
这个就是整个项目的简单流程,包括了数据的上传,后台的管理以及渲染的获取和兼容接口失败的数据获取。
读取本地语言包
首先我们要做的就是,读取本地语言包文件,把本地数据转换成json,并准备上传到服务器,我们用 fs.readdir 读取项目固定文件夹下面的语言包数据,所以开始的项目语言包最好是放在同一文件夹下面,这样就方便我们读取数据和写入数据的处理。下面是对应代码的展示,需要注意的是 我们需要用path.join 去获取文件路径。
1async function languageLoad() {
2 const localPath = path.join(__dirname, '../src/i18n/change');
3 fs.readdir(localPath, async (err, files) => {
4 if (!err && files && Array.isArray(files)) {
5 for (const file of files) {
6 const keyPath = path.join(__dirname, `../src/i18n/change/${file}`);
7 const jsonstring = await fs.readFileSync(keyPath, 'utf8');
8 const data = JSON.parse(jsonstring);
9 }
10 }
11 await changeRead();
12 });
13}
上传语言包到服务器
读取到对应的本读语言包数据之后,我们就将语言包上传到服务器,这个过程如下,在这里我们就通过请求,把数据传给服务器,并且我们需要做好日志的打印,方便查看上传的状态,因为一般比较大的项目,国际化数据是很庞大的,而且如果是多语言的话,每一种语言都需要上传。
1async function uploadFile(lngType) {
2 try {
3 const jsonstring = fs.readFileSync(path.join(__dirname, `../src/i18n/locales/${lngType}.json`), 'utf8');
4 const res = await axios.post(url, {
5 languageJson: JSON.parse(jsonstring),
6 });
7 if (res.data.code === 0) {
8 console.log('读取文件失败', `${lngType}.json`, res.data);
9 failNum += 1;
10 } else {
11 successNum += 1;
12 }
13 } catch (error) {
14 console.log('读取文件失败', `${lngType}.json`);
15 failNum += 1;
16 }
17}
自动化上传
当然上传语言的方式很多,既可以通过手动上传,也可以通过自动化上传,目前我们采用的是自动化上传,也就是在项目部署时,通过执行build命令,我们可以完成自动上传语言包。主要是通过在部署脚本中添加相应的命令, 下面是对应的说明,我们在check.js文件中,执行了uploadFile方法,等待语言包上传完成 再执行后面的部署任务。
1"build": "node ./scripts/language.js && next build"
完成这一步之后,我们前半段的任务就算结束了,主要是读取本地文件和本地语言包的上传,所以到这一步我们基本就成功了一半。就下来,既然我们的语言包都上传了,我们应该怎么去管理它,这就是我们的第三步,搭建一个后台管理系统。
3. 语言包管理平台
为了更好地管理语言包,我们搭建了一个语言包管理平台。这个平台允许对项目和项目的语言包进行增、改等操作,并记录操作日志。这使得管理国际化的key变得更加方便和高效。如果某个key的值发生变化,我们可以直接在后台调整,无需修改项目代码并重新发布。当然这个后台管理系统相对来说比较简单,主要的就是及时修改对应的key,方便更新,在实效性上就有巨大优势的。
这张图就是对项目进行管理的后台,主要的功能就是去管理不同的项目,因为我们的国际化项目较多,并不是单一的一个项目,所以首先我们就创建了项目的管理,针对项目的默认语言和项目的key,统一处理。可以看到目前我们已经管理了9个项目。
这个是对需要翻译的每一个key进行管理,主要包括key的搜索,内容的搜索,以及对应key所属项目的搜索,这样就很方便的对每一个翻译只进行了管理,同时可以看到它所对应的每一种翻译的值。
当然我们不仅仅到这就结束了,这个是操作按钮的相关展示,我们提供了对key的修改,可以对每一种语言进行调整,如果有错误的,或者需要更新的时候,直接在这修改之后,在C端页面就能立即生效。是不是很方便,不用再在项目里面修改,修改完之后还需要重新部署上线,这个效率真的是得到了巨大的提升。
当然管理后台可以按照公司规划或者设计要求进行不同的设计,只要主要的功能实现了,别的都是锦上添花。现在前期的工作我们就算做完了,然后我们就来说说,怎么在项目里面使用远程的数据。
4. 优先使用本地语言包
本地优先策略
这里在线上的展示我们就是直接获取数据然后渲染的我们就不在详细的说了哈,感兴趣的小伙伴可以在评论区留言,我们会第一时间回复。这里主要说一下在本地开发和测试时,我们怎么优先使用本地的语言包数据,以便实时查看修改效果。因为本地开发,多个需求经常时同时进行,你开发你的,我开发我的,总不能都用线上的数据吧,那我本地的怎么生效,这就是我们要说的重点了,下面是具体的实现代码:
首先我们分成了两种请求方式先拿出i18n的通用参数:
1const commonParams = {
2 interpolation: {
3 escapeValue: false,
4 },
5 load: "languageOnly",
6 debug: false,
7 fallbackLng: "zh_CN",
8 initImmediate: false,
9};
然后我们写一个从本地获取数据的方法,这里的路径就是你本地语言包的路径:
1const localParams = {
2 backend: {
3 backends: [
4 resourcesToBackend((lng) => {
5 return import(`./locales/${lng}.json`);
6 }),
7 ],
8 },
9};
最后我们写一个从接口获取数据的方法:
1const lineParams = {
2 backend: {
3 loadPath: (lngs) => {
4
5 return `${basemainLng}/i18n/get?projectName=${projectName}&languageType=${templng}`;
6 },
7 request: async (options, url, payload, callback) => {
8 await axios
9 .get(url)
10 .then((res) => {
11 callback(null, {
12 status: 200,
13 data: res.data.data,
14 });
15 })
16 .catch(() => {
17 localInit();
18 });
19 },
20 parse: (data) => {
21 return JSON.parse(data || "{}").data;
22 },
23 },
24};
最后就是终点了,在i18n初始化的时候,我们判断是一下当前的环境是在本地还是在环境上,如果是本地local的话,就初始化localInit,反之就初始化lineInit,这样我们就顺利的实现了,本地和线上的数据获取问题。当然渲染渲染的时候,直接调用lineInit的初始化就可以了。
1const localInit = () => {
2 i18n.use(ChainedBackend)
3 .use(initReactI18next)
4 .init({ ...commonParams, ...localParams });
5};
6
7
8const lineInit = () => {
9 i18n.use(Backend)
10 .use(initReactI18next)
11 .init({ ...commonParams, ...lineParams });
12};
13
14
15const i18nInit = () => {
16 return local ? localInit() : lineInit();
17};
接口报错兼容处理
但是,还有一点很重要,就是有人会说,服务器不稳定,接口如果报错了,你不是就没有数据渲染了吗,哪怕是要跑路了。哈哈哈,如果在获取远程语言包时发生错误,我们将自动回退到本地语言包数据。这种处理方式是不是就大大提高了系统的健壮性和稳定性,看看我们是怎么去实现的:
1request: async (options, url, payload, callback) => {
2 await axios
3 .get(url)
4 .then((res) => {
5 callback(null, {
6 status: 200,
7 data: res.data.data,
8 });
9 })
10 .catch(() => {
11 localInit();
12 });
13}
对,就是很简单,在i18n请求的时候,在catch里面加一个报错的兼容,因为前面的代码我们都写好了,所以如果这个时候报错了,直接就初始化的是本地的语言包数据,通过这种方式,即使接口数据获取失败,系统仍能正常使用本地的语言包,确保应用的正常运行。到这里我们的任务就基本全部完成了,但是还有一点比较重要,那就是怎么保持数据的最新态。
5. 更新本地语言包数据
有时候你本地分支的代码,可能已经被污染了,有一些错误的数据,或者有一些数据我们在后台已经进行了调整,但是代码里面的还没有更新过来。这个时候,为了保持本地和远程语言包数据的一致性,我们在每次项目启动时都会自动执行脚本命令,从远程获取到最新的数据,然后去更新本地文件里面的语言包数据。这一过程包括读取本地修改的语言文件、获取远程的最新语言数据,并将其与本地数据合并。这样,我们可以确保使用的语言包始终是最新的版本,同时也保留本地的修改记录。
以下是实现这一功能的代码:
首先我们先定义一些变量来存储数据:
1let wrSuccessNum = 0;
2let wrFailNum = 0;
3let localKeys = [];
4const localJson = {};
然后我们就是获取远程的数据,并结合本地的语言数据,重新组合,然后把新的数据,写入到对应的语言包文件里面,但是需要做好对应的日志,方便你及时看到有哪些数据报错了,没有获取到等:
1async function settingLng(lngType) {
2 try {
3 const url = `${basemain}/i18n/get?projectName=${PROJECTNAME}&languageType=${lngType}`;
4 const res = await axios.get(url);
5 if (res.data.data) {
6
7 await fs.writeFileSync(lngPath, writeData);
8 wrSuccessNum += 1;
9 } else {
10 wrFailNum += 1;
11 console.log(`写入文件失败:${lngType}.json`);
12 }
13 } catch (error) {
14 wrFailNum += 1;
15 console.log(`写入文件失败:${lngType}.json`);
16 }
17}
所有的这些工程都是自动化的,不需要开发进行任何操作,只要你启动了项目,然后就会更新,并且输出最新的数据到本读,当然这个数据并不会覆盖本地的数据,而是你本地调整的数据和远程新增的数据的结合。通过这个更新过程,我们确保了本地语言包和远程服务器上的语言包保持同步。如果有新的修改或新增的翻译内容,我们在项目启动时就会自动运行这段代码,然后自动更新本地数据。这不仅保证了数据的一致性,还提升了开发效率和项目的国际化支持能力。到这我们的所有流程的代码就结束了,如果完全的实现了上面的,那么祝贺你,你也有了自己的国际化管理。
总结
通过本文介绍的方案,我们成功地在 React 项目中实现了语言包的远程管理和本地优先加载。这种解决方案不仅提高了国际化工作的效率,还确保了开发和生产环境中的灵活性和稳定性。目前来看这个方法主要有以下的几个优点:
方案的优点
-
自动化管理:通过自动化脚本,我们实现了语言包的自动上传和更新,避免了手动管理的繁琐和易错。这不仅提高了工作效率,还减少了可能的人为失误。
-
灵活的本地和远程切换:在本地开发时,我们优先使用本地的语言包数据,使得开发者可以快速看到修改效果。而在生产环境中,我们则从远程服务器获取最新的语言包数据,确保用户看到的是最新的内容。
-
健壮的错误处理机制:我们为语言包的获取设置了本地和远程两套数据源,并在接口报错时优先使用本地数据。这一机制提高了系统的稳定性,即使远程服务器出现问题,应用仍能正常显示语言内容。
-
统一的数据源管理:通过集中管理平台,我们可以对各个项目的语言包进行统一管理,包括新增、修改和删除操作。这大大简化了多语言内容的维护和更新过程,减少了由于数据不一致导致的问题。
-
维护性和扩展性:未来,我们可以轻松地为新的语言或新的项目添加支持,只需在管理平台上进行相应的配置即可。这样的架构设计使得系统具有良好的扩展性和维护性。
未来展望
目前国际化的东西我们也在持续的更新中,在未来,我们计划进一步优化和扩展这一流程。可能的改进包括:
-
引入版本控制:为语言包增加版本控制功能,以便追踪和回滚修改,确保所有修改可追溯。
-
更智能的语言包更新策略:例如,基于用户的语言偏好或使用频率,智能地选择性加载必要的语言包,以优化性能和用户体验。
-
用户反馈机制:在应用中加入用户反馈机制,允许用户直接反馈翻译问题,从而持续优化语言内容的质量。
-
自动翻译的接入:随着AI大模型的升级,自动翻译已经是很方便了,怎么把需要翻译的内容直接转成对应的语言我们会在未来持续跟进。