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

具體的實現就不展開了,那將是另一篇文章...

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章