Umi 小白纪实(五)—— 结合有道翻译 API 实现 i18n 多语言功能
多语言(国际化)是一个很常见的需求,Umi 对多语言也有很好的支持
一、简单实现
Umi 基于 react-intl 封装了多语言插件 @umijs/plugin-locale
不过并不需要单独引入,只需要在配置文件(.umirc.js 或 config/config.js)中配置 locale
export default {
locale: {
// 默认语言
default: 'zh-CN',
// antd 启用国际化
antd: true,
// 浏览器页面标题支持国际化
title: true,
// 浏览器语言检测
baseNavigator: true,
},
};
然后在 src/locales 目录下创建所需要的语言包
src
├── locales
│ ├── en-US.js
│ ├── zh-CN.js
│ └── zh-TW.js
└── pages
// zh-CN.js
export default {
HOME_PAGE: '首页',
PAGE_NOT_FOUND: '页面不可见',
};
// en-US.js
export default {
HOME_PAGE: 'Home page',
PAGE_NOT_FOUND: 'Page not found',
};
然后在页面中通过 useIntl 拿到 formatMessage 方法,处理需要国际化的文本
import React from 'react';
import { useIntl } from 'umi';
const Demo= () => {
const { formatMessage } = useIntl();
return <div>{formatMessage({ id: 'HOME_PAGE' })}</div>;
};
最后在切换语言时调用 setLocale,实现多语言切换
import React from 'react';
import { setLocale } from 'umi';
const Demo = () =><button onClick={() => setLocale('en-US')}>English</buttn>;
二、更完善的多语言切换
上面是使用 setLocale 实现的多语言切换,这样的实现并不完善
比如:<htm /> 标签上的 lang 属性并没有改变,第三方库( moment.js )并没有切换语言
这些功能实现的前提,是知道当前项目是哪一种语言,这可以通过 getLocale 来获取
import { getLocale } from 'umi';
console.log(getLocale()); // en-US | zh-CN
也可以通过 localStorage 中的 umi_locale 字段来获取,因为 setLocale 会更新 umi_locale
最终就能实现一个简易的自定义 hooks
// useChangeLocale.js
import { useMemoizedFn } from 'ahooks';
import { setLocale, getAllLocales } from 'umi';
import moment from 'moment';
export const STORAGE_LOCALES_KEY = 'umi_locale';
export const Locales = {
ZH_CN: 'zh-CN',
ZH_TW: 'zh-TW',
EN_US: 'en-US',
};
// 获取默认语言
export function getDefaultLocale() {
const allLocales = getAllLocales();
const systemLanguage = navigator.language;
return (
// 本地缓存
localStorage.getItem(STORAGE_LOCALES_KEY) ||
// 系统默认语言
(allLocales.includes(systemLanguage) && systemLanguage) ||
// 中文
Locales.ZH_CN
);
}
// 修改 <html /> 标签中的 lang 属性
export function setLocaleToHTMLLang(locale = getDefaultLocale()) {
const html = document.querySelector('html');
html && (html.lang = locale);
}
// 切换语言
export const useChangeLocale = () => {
const changeLocale = useMemoizedFn((locale) => {
localStorage.setItem(STORAGE_LOCALES_KEY, locale);
setLocaleToHTMLLang(locale);
setLocale(locale);
moment.locale(locale);
});
return changeLocale;
};
然后在切换语言的时候,将原本的 setLocale 改为 useChangeLocale
import React from 'react';
import { useChangeLocale } from '@/hooks/useChangeLocale';
const Demo = () => {
const changeLocale = useChangeLocale();
return <button onClick={() => changeLocale('en-US')}>English</buttn>;
};
三、通过脚本翻译语言包
现在已经实现了多语言功能,但多语言功能最大的工作量并不是“实现”,而是语言包
如果需要精准的翻译,还是需要请专业的翻译人员来维护语言包
如果要求不是很严格,能接受机翻的话,就很有多的操作空间了
1. 繁体中文
node.js 环境有一个 chinese-conv 插件可以实现简繁转换
基于此,可以写一个翻译脚本,将 zh-CN.js 翻译为 zh-TW.js
在根目录创建脚本文件 scripts/translateCNToTW.js
// translateCNToTW.js
const fs = require('fs');
const path = require('path');
const chineseConv = require('chinese-conv');
const LOCALES_PATH = path.join(__dirname, '../src/locales');
console.log('同步 zh-CN.js 到 zh-TW.js 中...');
const text = fs.readFileSync(path.join(LOCALES_PATH, 'zh-CN.js'), {
encoding: 'utf-8',
});
fs.writeFileSync(
path.join(LOCALES_PATH, 'zh-TW.js'),
// 读取 value 部分,并转换为繁体中文
text.replace(
/'([^']*)',$/gm,
(_, origin) => `'${chineseConv.tify(origin)}',`,
),
{
encoding: 'utf-8',
},
);
console.log('同步完成');
2. 英文
简繁转换有 chinese-conv,翻译为英文可以使用第三方 API
我使用的是有道智云,然后通过 youdao-node 进行翻译
const { default: youdao } = require('youdao-node');
youdao.config({
appKey: 'your appKey',
appSecret: 'your appSecret',
});
async function translateToEN(content) {
try {
console.log(`翻译中....${content}`);
const res = await youdao.translate({
content,
from: 'zh-CHS',
to: 'EN',
});
return res.translation[0] || content;
} catch (e) {
console.log(e);
return content;
}
}
第三方翻译 API 通常会存在次数限制,为了节省资源,可以对翻译的结果做一个缓存 enCache.json
在执行脚本的时候,首先跳过已有 en-US 中已有的结果,然后先从 enCache.json 中读取,都没有的情况下再调翻译 API
// translateCNToEN.js
const fs = require('fs');
const path = require('path');
const pMap = require('p-map');
function readFile(filePath) {
return fs.readFileSync(path.join(__dirname, filePath), {
encoding: 'utf-8',
});
}
function writeFile(filePath, data) {
return fs.writeFileSync(path.join(__dirname, filePath), data, {
encoding: 'utf-8',
});
}
// 通过正则提取语言包中的 key-value
function fromEntries(textFile) {
const list = [];
const map = {};
textFile.replace(/(\S+):\s+'([^']*)',/gm, (_, $1, $2) => {
map[$1] = $2;
list.push({
key: $1,
word: $2,
});
return '';
});
return { list, map };
}
console.log('同步 zh-CN.js 到 en-US.js 中...');
// 需要提前创建 enCache.json
const cacheData = JSON.parse(readFile('./enCache.json'));
// 获取所有的中文 [{ key, word }]
const zhCNText = readFile('../src/locales/zh-CN.js');
const { list: zhList } = fromEntries(zhCNText);
// 获取所有的英文 [{ key:value }]
const enUSText = readFile('../src/locales/en-US.js');
const { map: enMap } = fromEntries(enUSText);
(async function () {
// 使用 pMap 开启异步遍历任务
const jsonDictEntries = await pMap(
zhList,
async (item, index) => {
const { key, word } = item;
// 优先调用缓存中的数据,若无再调用有道接口
let value = enMap[key] || cacheData[word];
if (!value) {
try {
value = await translateToEN(word);
} catch (e) {
console.log(`翻译「${word}」出错`);
console.log(e);
}
}
return value ? [word, value] : [];
},
{ concurrency: 5 },
);
const jsonDict = Object.fromEntries(jsonDictEntries);
// 写入缓存
writeFile('./enCache.json', JSON.stringify(jsonDict, null, ' '));
// 将翻译结果同步到 en-US.js
writeFile(
'../src/locales/en-US.js',
zhCNText.replace(
/'([^']*)',$/gm,
(_, origin) => `'${jsonDict[origin] || origin}',`,
),
);
console.log('同步完成');
})();
四、自动执行脚本
翻译脚本创建完毕,接下来可以在 package.json 中增一个命令
{
"scripts": {
"locales": "node scripts/translateCNToTW.js && node scripts/translateCNToEN.js"
}
}
然后在终端执行 npm run locales 就能执行翻译脚本
但每次都手动执行也太不智能了,这时候可以引入 husky + lint-staged,在每次 commit 的时候执行 npm run locales
// . lintstagedrc { "src/locales/zh-CN.js": [ "npm run locales", "git add src/locales/*", "git add scripts/*" ] }