多語言(國際化)是一個很常見的需求,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/*" ] }