元件庫設計實戰系列:國際化方案

誠身發表於2017-11-04

放眼全球,中國整體的網際網路技術實力毫無疑問僅次於美國並領先剩餘所有的國家一大截。但如果我們非要找出一箇中國網際網路公司做得不夠優秀的地方,那麼產品國際化一定是其中之一。雖然我們也擁有諸如 AliExpress,天貓國際等成功案例,但不得不說大部分中國公司在選擇出海後,都沒有能夠收穫到與預期相匹配的回報。這其中原因自然很多,然而缺乏一套可以平臺化,產品化的通用國際化方案一直都是其中一個非常重要的原因。

曾經筆者也天真地認為國際化不過是幾個 json 檔案的鍵值對匹配,但在深入瞭解了一些產品的國際化需求後,筆者才意識到要做一套好的國際化方案並沒有那麼簡單。

服務端國際化

對於前端工程師而言,國際化所要面臨的第一個挑戰就是,並不是所有的資料都可以在前端做國際化。常見的例子如電商類產品的貨品或商家資訊,這些都是有強更新需求,需要儲存在後端資料庫中,通過產品後臺進行更新的。如果一個商品要銷往美國,德國,法國,西班牙,泰國,印度尼西亞,而運營人員又只想維護一套以中文為基準的商品資訊,那麼這類資料的國際化我們就需要將其做在服務端。

我們當然可以麻煩後端工程師幫助我們根據每個請求的域名或 HTTP header 中的 content-language 來返回不同表中的翻譯,但如果你是一位致力於向全棧方向發展的前端工程師,不妨可以嘗試將國際化這一需求服務化,使用 Node.js 來封裝一個國際化中介軟體,在每個請求返回前對其返回值進行翻譯處理。

因為每個公司的技術架構不同,我們暫且略過技術細節不表。但我們需要知道的是,相較於前端國際化,後端介面的國際化其實更為關鍵與重要。因為這涉及到我們是否能將我們的核心資料以使用者可理解的語言展現出來,而國際化也絕不僅僅是將幾個字串翻譯為對應語言那樣簡單。

哪些資料需要做國際化

在討論具體的國際化方案之前,我們首先要明確一個問題,那就是產品中的哪些資料是需要做國際化的。

簡而言之,除去後端返回的資料,所有在前端渲染的單詞,語句,以及巢狀在其中的資料,都需要做相應的國際化。對應到程式碼層面,需要保證程式碼中沒有任何一行硬編碼的字串與符號。不論是大到一個區塊標題,還是小到一個確認按鈕的文案,所有的展示資訊都需要做國際化。

鍵值對匹配與多語言支援

回到前端,讓我們從最簡單的國際化場景說起。

例如下拉選單輸入框中的“選擇”佔位符,假設我們需要同時將其翻譯為英文與法文,首先我們需要引入兩個語言檔案:

// en-US.json
{
  "web_select": "Select"
}

// fr-FR.json
{
  "web_select": "Sélectionner"
}複製程式碼

並提供一個全域性的 localeUtil.js,支援傳入語言型別與 key 值,並返回相應的翻譯。

這裡提供兩點最佳實踐。

一是將不同語言的翻譯存在獨立的 json 檔案中。雖然我們可以使用巢狀的資料結構將所有翻譯都儲存在一個 locale.json 裡面,但考慮到生產環境中語言檔案一般都是按需載入的,所以根據不同的語言存在對應的獨立的的 json 檔案中顯然是一個更好的選擇。

二是同一語言中 key 值的命名,同樣不建議採取巢狀的結構。扁平化的語言檔案可讀性更強,取值時的效率也更高,同時也可以使用下劃線來區別不同的層級,如 web_homepage_banner_title,即平臺_頁面_模組_值,當然具體情況也可以按需調整。

模板匹配與條件運算子

瞭解了最簡單的場景,我們再來考慮一個複雜些的用例。

在顯示商品價格時,為了可擴充套件性等多方面的考慮,後端在設計表結構時,是不會將商品價格直接儲存為字串的,而是拆分為貨幣符號(string 型別)及價格(float 型別)。而在前端顯示時,我們經常會遇到要將其渲染為一句促銷語的場景,如:

2017年9月1日前購買,只需100元。
複製程式碼

對於時間類資料的國際化方案,我們這裡先暫時按下不表,有興趣的同學可以研究一下 moment.js 的實現,moment.js 也是目前前端屆日期國際化的代表。

由於100元是一個動態的變數,所以我們的 localeUtil.js 還需要支援傳入變數,這裡一個常用的呼叫可以為:

localeGet(
  'en-US', // locale
  'web_merchantPage_item_promotion', // key
  { currency: item.currency, promoPrice: item.promoPrice }, // variable
);複製程式碼

語言檔案中的模板可以為:

"web_merchantPage_item_promotion": "Before YYYY/MM/DD, purchase at {currency} {price}."複製程式碼

另一個常見的場景為英文名詞的單複數問題,這裡我們選擇通過條件運算子的思路來解:

優惠將於3天后結束。
複製程式碼
"web_merchantPage_item_promotion_condition": "Promotion will end in {count, =1{# day} other{# days}}",複製程式碼

資料國際化

除去日期,貨幣外,數字也是字串之外另一個國際化的難點,我們來看下面這個例子。

阿里巴巴向印度尼西亞電商網站 Tokopedia 注資11億美金。
Alibaba leads $1.1b investment in Indonesia’s Tokopedia.
複製程式碼

這裡我們需要將“11億美金”翻譯為“$1.1b”,為了達到這一目的,我們首先需要在各個語言檔案中建立對應語言的基礎單位 mapping,如:

// zh-CN
"hundred": "百",
"thousand": "千",
"ten_thousand": "萬",
"million": "百萬",
"hundred_million": "億",
"billion": "十億",

// en-US
"hundred": "hundred",
"thousand": "thousand",
"thousand_abbr": "k",
"million": "million",
"million_abbr": "m",
"billion": "billion",
"billion_abbr": "b",複製程式碼

然後我們需要實現一個可以將浮點數進行純數字與單位轉換的函式,返回純數字與所使用語言的單位 key 值:

function formatNum(num, isAbbr = false) {
  ...
  return {
    number: number, // 1.1
    unit: unit, // "billion_abbr"
  }
}複製程式碼

接著就可以呼叫 localeGet 來獲得相應的翻譯:

localeGet(
  'en-US',
  'news_tilte',
  {
   number: 1.1,
   unit: localeGet('billion_abbr'),
   currency: localeGet('currency_symbol'),
  },
)複製程式碼

語言檔案中的模板如下:

// zh-CN
"news_tilte": "阿里巴巴向印度尼西亞電商網站 Tokopedia 注資{number}{unit}{currency}。"

// en-US
"news_tilte: "Alibaba leads {currency}{number}{unit} investment in Indonesia's Tokopedia."複製程式碼

在整個過程中,我們可以抽象出兩種解決問題的思路。

一是拆分並抽象出基礎資料,如單位等。

二是靈活運用模板與變數,將其調整為最符合當地使用者閱讀習慣的翻譯。

類似的思想也可以推廣到處理日期,小數,分數,百分數等。

React 下的國際化方案

正如前文中所提到的,按需載入語言檔案是國際化方案中必要的一環。簡而言之,我們可以在專案的入口檔案中載入所需的語言檔案,但考慮到整體專案的統一性,我們最好可以將語言檔案掛載在全域性 redux store 下的一個分支,以使得每個頁面都可以通過 props 方便地進行取值。而且,在 redux store 的層面載入語言檔案,可以保證所有頁面使用的都是同一份語言檔案,後續也不需要在 localeGet 函式中傳入具體的 locale 值。

示例程式碼如下:

import enUS from 'i18n/en-US.json';

function updateIntl(locale = 'en-US', file = enUS) {
  store.dispatch({
    type: 'UPDATE_INTL',
    payload: {
      locale,
      file,
    },
  });
}複製程式碼

這樣我們就可以方便地將語言檔案掛載在 redux store 的一個分支下:

const mapStateToProps = (state) => ({
  intl: state.intl,
});

// usage
localeGet(this.props.intl, 'web_select');

// with defaultValue to prevent undefined return
localeGet(this.props.intl, 'web_select', 'Select');複製程式碼

其他

除了上述提到的這些問題之外,在生產環境中我們還需要注意以下兩點:

  • HTML 轉義字元
  • 特殊語言的 unicode 轉碼,如簡體中文,繁體中文,泰語等

正如開篇時提到的,國際化是一個通用的系統性工程,以上提到的這些點也難免掛一漏萬,更多的最佳實踐還需要在實際開發工作中持續提煉,總結,沉澱。

小結

對於任何一家希望開拓國際市場的公司來說,產品國際化都是一個剛需。從技術人員的角度來講,我們當然可以滿足於一個 Node.js 中介軟體或一個前端的 npm 包來通用地解決這一問題。但事實上,我們還可以再向前一步,那就是將國際化這個服務做成一個完整的 SASS 產品,這方面成功的案例如:OneSky

OneSky 所提供的額外功能,如雲端儲存,多檔案型別支援,多人實時翻譯協作等,每一個功能單拿出來都又是一個新的領域,而這也正是服務產品化的難點所在。

舉例來說,前文中提到的國際化方案,都是預設所有翻譯工作已經完成且 json 化完畢,可以直接 import 到專案中使用,而這也就是技術人員經常會陷入的一個思維盲區。翻譯數量龐大的語言檔案本身就是一件非常困難的事情,如何讓身處世界各地的非技術背景的翻譯人員進行協作並方便生產環境中的產品實時更新語言檔案,這些問題都只有在把國際化服務做成一個成熟的商業產品之後才會被考慮到。

事實上,目前在各大網際網路公司中,技術服務產品化已經成為了一股不可阻擋的趨勢,許多技術出身的工程師都已經開始意識到一套只有技術人員才能理解並使用的解決方案是不夠的,只有將這些“高深莫測”的技術服務產品化,傻瓜化,才能夠開啟一片更大的戰場,使技術真正服務於商業產品並在現實世界中產生更大的價值。


相關文章