組件庫設計實戰 - 國際化方案
關鍵詞
- 國際化方案
- 服務產品化
前情提要
距離上一篇組件庫設計實戰文章推出已經過去七個月了,但接下來兩篇文章所要涉及的兩大主題(國際化與複雜組件)其實早就已經確定下來,之所以未能發出是因為筆者一直都糾結於文章中所提出的方案是否觸及到了這兩個主題的本質。單純的代碼分享並不能幫助廣大的開發者解決各自生產環境中具象的問題,而 pure render 專欄一直都希望讀者可以從分享中獲得更多的 insights,可以將具體的問題推而廣之,從而獲得更高維度上的提升。
國際化方案
註:以下所討論的國際化方案,包括但不僅限於組件庫
放眼全球,中國整體的互聯網技術實力毫無疑問僅次於美國,且領先剩餘所有的國家一大截。但如果我們非要找出一個中國互聯網公司做得不夠優秀的地方,那麼國際化一定是其中之一。雖然我們擁有諸如 AliExpress,天貓國際等成功案例,但不得不說大部分中國公司在選擇出海後,都沒有能夠收穫到與預期相匹配的回報。這其中原因自然很多,然而缺乏一套可以平台化,產品化的通用國際化方案一直都是其中一個非常重要的原因。
曾經筆者也天真地認為國際化不過是幾個 json 文件的鍵值對匹配,但在深入了解了一些產品的國際化需求後,筆者才意識到一套好的國際化方案並沒有這麼簡單。
服務端國際化
對於前端工程師而言,國際化所要面臨的第一個問題就是,並不是所有的數據都可以在前端做國際化的。常見的例子如電商類產品的貨品或商家信息,這些都是有強更新需求,需要存儲在後端資料庫,通過產品後台進行更新的。如果一個商品要銷往美國,德國,法國,西班牙,泰國,印度尼西亞,而運營人員又只想維護一套以中文為基準的商品信息,那麼這類數據的國際化我們就需要將其做在服務端。
我們當然可以麻煩後端工程師幫助我們根據每個請求的域名或 HTTP header 中的 content-language 來訪問不同表中的翻譯,但如果你是致力於向全棧發展的前端工程師,不妨可以嘗試將國際化這一需求服務化,使用 nodejs 來封裝一個國際化中間件,在每個請求返回前對其返回值進行翻譯處理。
這裡因為每個公司的技術架構不同,我們暫時略過技術細節不表,但我們需要知道的是,相較於前端國際化,後端介面的國際化其實更為關鍵與重要。因為這涉及到我們是否能將我們的核心數據以用戶可理解的語言展現出來,而國際化也絕不是將幾個字元串翻譯為對應語言那樣簡單。
哪些數據需要做國際化
在我們討論具體的國際化方案之前,我們首先要明確一個問題,那就是產品中的哪些數據是需要做國際化的。
簡而言之,除去後端返回的數據,所有在前端需要渲染的單詞,語句,以及嵌套在其中的所有數據,都需要做相應的國際化。對應到代碼層面,就需要保證代碼中沒有任何一行硬編碼的字元串與符號。不論是大到一個區塊標題,還是小到一個確認按鈕的文案,所有展示信息都需要做國際化。
鍵值對匹配與多語言支持
回到前端,讓我們從最簡單的國際化場景說起。
例如下拉列表輸入框中的「選擇」佔位符,假設我們需要同時將其翻譯為英文與法文,首先我們需要引入兩個語言文件:
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", "web_merchantPage_item_promotion", { currency: item.currency, promoPrice: item.promoPrice },);
語言文件中的模板可以為:
"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) => { const intl = state.intl; return { messages: intl, };};// usagelocaleGet(this.props.messages, "web_select");// with defaultValue to prevent undefined returnlocaleGet(this.props.messages, "web_select", "Select");
更為具體的實現方案,大家可以參考:react-intl-redux。
其他
除了上述提到的這些問題之外,在生產環境中我們還需要注意以下幾點:
- HTML 轉義字元
- 特殊語言的 unicode 轉碼,如簡體中文,繁體中文,泰語等
正如開篇時提到的,國際化是一個通用的系統性工程,以上提到的這些點也難以掛一漏萬,更多的最佳實踐還需要大家一起在實際開發工作中持續提煉,總結,沉澱。
結
細心的讀者可能注意到我們在開頭時為國際化方案所定的關鍵詞為:服務產品化,而這也是筆者在最後最想與各位分享的一點。
對於任何一家希望開拓國際市場的公司來說,產品國際化都是一個剛需。從一個技術人員的角度考慮,我們當然可以止步於一個 nodejs 中間件或一個前端的 npm 包來通用地解決這一問題。但事實上,我們還可以再向前一步,那就是將國際化這個服務做成一個完整的 SASS 產品,這方面成功的案例如:OneSky。
OneSky 所提供的額外功能,如雲端存儲,多文件類型支持,多人實時翻譯協作等,每一個功能的解決方案單拿出來都又是一個新的領域,而這也正是服務產品化的難點所在。
舉例來說,前文中提到的國際化方案,都是默認所有翻譯工作已經完成且 json 化完畢,可以直接 import 到項目中直接使用。而這就是技術人員經常會陷入的一個思維誤區,翻譯數量龐大的語言文件本身就是一件非常困難的事情,如何讓身處世界各地的非技術背景的翻譯人員進行協作並方便生產環境中的產品實時更新語言文件,就是想要將國際化方案產品化的公司需要解決的問題。
事實上,在各大互聯網公司中,技術服務產品化已經成為了一股不可阻擋的趨勢,許多技術出身的工程師都已經開始意識到只有一套技術人員可以理解並使用的解決方案是不夠的,只有將這些「高深莫測」的技術服務產品化,傻瓜化,才可以打開一片更大的戰場,使技術真正服務於產品,在現實世界中產生更大的價值。
推薦閱讀:
※React Conf 2017 不能錯過的大起底——Day 1!
※解析 Redux 源碼
※我對Flexbox布局模式的理解
※從0到1,細說 React 開發環境搭建那點事
※Immutable 詳解及 React 中實踐