Webnovel 國際化實踐

閱文前端團隊發表於2018-12-27

本文作者:張卓

原創宣告:本文為閱文前端團隊 YFE 成員出品,請尊重原創,轉載請聯絡公眾號 ( id: yuewen_YFE ) 獲取授權,並註明作者、出處和連結。

前言

Webnovel(起點海外專案)在今年開始了國際化的腳步,在剛剛上線的版本當中加入了對印尼、馬來西亞和菲律賓語言及內容的支援。在做國際化的過程中,我們遇到了不少問題,這篇文章就重點分享一下這些問題以及它們的解決方案。

國際化和本地化

在開始之前,我們先明確兩個概念: 國際化和本地化。國際化(i18n) 是一個設計和準備應用程式的過程,使其能用於不同的語言。 而 本地化(l10n) 是一個把國際化的應用針對部分割槽域翻譯成特定語言的過程。這篇文章的標題是“國際化”實踐,所以重點講的也是如何準備應用程式讓其能夠進行本地化。

需要解決的問題

將一個網站進行多語言化看似是一件簡單的事情,在大多數情況下的確是的,只是將一個字串對映到另一個字串的過程,但是起點海外作為一款有追求的產品,我們當然不會採用這麼簡單的方式。要把國際化這件事情做好,那就會遇到許多問題,例如單複數、富文字等。下面的部分會介紹一些常見的多語言問題以及一些通用的解決方案:

單複數問題

在我們中文當中,沒有單複數的概念,“一小時”和“兩小時”中的“小時”是一樣的,但在其它許多語言當中,不同數量的形式,有著不同的規則,英文中有單數和複數兩種規則,如“1 hour” 和 ”2 hours”,而有的語言可能有更多。在一些語言中,基數和序數的規則可能也是不同的,如英文的“1st”,“2nd”,“3rd”,“4th”。

Unicode 標準已經將世界上絕大部分語言的單複數規則進行了歸類,總結下來最多隻有 6 種規則,分別是:

  • zero
  • one
  • two
  • few
  • many (如果有一個單獨的分類的話,也用於分數)
  • other (必須,如果語言只有一個單一形式也使用)

阿拉伯語基數的單複數規則

如英文中,基數只有 one (1 hour)和 other (0 hours,2.5 hours)兩種規則,序數則有 one(1st,11st…),two(2nd),few(3rd),和 other(4th)四種規則。

規則是有了,我們如何在實際的應用中使用呢?一種比較通用的方式是使用 ICU MessageFormat。ICU MessageFormat 是一種語法格式,通過 {key} 的形式來定義變數;通過一些關鍵詞來幫助我們更方便的處理不同語言中的一些複雜情況,例如使用 {key, plural, matches}來處理單複數規則:

You have {itemCount, plural,
    =0 {no items}
    one {1 item}
    other {{itemCount} items}
}.
複製程式碼

ICU MessageFormat 不僅可以方便單複數情況的使用,同樣可以用於日期、性別以及其它複雜情況,並且已經擁有了非常廣泛的使用,不僅絕大部分 JavaScript 的多語言庫使用了它,在其他語言例如 Java 和 PHP 中也同樣內建了這套規則。

日期、數字以及貨幣 不同語言、國家和地區在表示日期和數字時也會有一些差異,如美國習慣“月/日/年”的形式來表示日期,而同樣使用英語的英國卻更習慣“日/月/年”。而貨幣就更不用說了,符號首先不同,如人民幣和日元的 “¥” 以及歐元的 “€”,並且它們放置的位置可能也不相同,日元習慣將貨幣符號放在數字前面,而歐元恰恰相反。

作為開發者,我們幾乎不可能去一一瞭解這些差異,好在 ECMAScript Internationalization API 提供了4個方法幫助我們解決上面的問題:

  • Intl.Collator
  • Intl.DateTimeFormat
  • Intl.NumberFormat
  • Intl.PluralRules

Intl.Collator 並不常用,主要是用於語言敏感字串比較的;Intl.DateTimeFormat 可以幫助我們根據不同地區語言格式化時間和日期;Intl.NumberFormat 則用來格式化數字和貨幣;而Intl.PluralRules 用於判斷單複數,可以告訴我們指定數量在某種語言下的分類(如 “one”,“other”)。其中前三個 API 已經相對穩定,瀏覽器也有較好的支援,DateTimeFormatNumberFormat 也有 polyfill 來讓我們有更廣泛的使用範圍,如 Node 和 React Native 環境中;PluralRules 還處在草案階段,瀏覽器支援性也比較差,不建議直接使用。

另外,我們看到瀏覽器尤其是 Chrome 對國際化的支援正在逐漸加大,除了上面已經進入標準的 4 個 API 外,Chrome 分別在 71 和 72 版本支援了 Intl.RelativeTimeFormatIntl.ListFormat 兩個 API,其中 Intl.RelativeTimeFormat 用來格式化相對時間,類似 Moment.js 中的功能,例如:

const rtf = new Intl.RelativeTimeFormat('en');
    
rtf.format(3.14, 'second');
// → 'in 3.14 seconds'
    
rtf.format(-15, 'minute');
// → '15 minutes ago'
複製程式碼

Intl.ListFormat 用來格式化列表,例如:

const lf = new Intl.ListFormat('zh');
    
lf.format(['永鋒', '新宇']);
// → '永鋒和新宇'
複製程式碼

這些 API 都遠比上面展示的例子強大,具體的用法可以參考 MDN 和 Google Developers 官網,也相信今後在 Web 上進行國際化會越來越容易。

含義和語境

我們知道,不論是中文還是其它語言,一個字/詞語在不同場景下可能會有不同的含義,“About” 如果當作一個頁面的標題,表達的可能是“關於/簡介”的含義,但放在一句話中就可能是“大約”的意思了。

對於這個問題,我們可以通過提供給譯者更多的資訊來解決這個問題。可通過文字描述幫助譯者來了解語境,傳送截圖等方式來確保譯者能夠準確的翻譯。

誰來翻譯

誰來翻譯,看起來似乎不是一個問題,但它決定著我們整個翻譯的流程,我們需要在進行多語言時儘早確定。一般來說,可能是由專業翻譯或者使用者/志願者來翻譯,這兩種方式各有優劣:

  • 由專業翻譯進行翻譯。專業翻譯基本可以保證較高的質量和效率,主要問題是成本較高。
  • 由使用者/志願者進行翻譯。很多開源專案都採用來這種方式。Twitter 也採用了這種方式,專門搭建了一個翻譯平臺來讓使用者更好的進行翻譯,在短短一年的時間內有超過 40 萬的志願者幫助其進行了翻譯,上線了 21 種語言。這種方式成本低,並且由於可能參與的人數眾多,通過多人 review 等方式可以保證翻譯質量;唯一需要擔心的是翻譯時間不可控。而我們一般也不需要自己搭建一套平臺,可以選擇已有的成熟的商業平臺如 crowdin.com 或者開源的平臺如 Mozilla 的 pontoon。

除了上面提到的多語言問題,我們在國際化的過程中可能還要面臨多方合作、分國際/地區運營等其它型別的諸多問題,這裡篇幅有限,就不一一討論了。

解決方案

在做多語言的 Web 應用時,一種比較通用的方式是:將不同語言的字元放在不同的 JSON 或其它形式的檔案當中,然後獲取使用者傾向的語言,載入對應語言的字元檔案,然後在應用中展示即可。

在這種方式下,需要解決的最大問題是一些比較複雜的情況,例如上文提到的單複數。在上文種我們也提到了可以通過 ICU MessageFormat 來解決這個問題,具體的做法是將 ICU MessageFormat 解析成 AST 然後轉化為函式,在應用中傳遞對應引數到對應函式即可。

上面的方式還有一些細節值得討論,篇幅原因這裡就不講了,接下來我們看一下相對比較成熟的基於主流框架的 i18n 解決方案。

React Intl

React Intl 是雅虎開源的基於 React 的國際化解決方案。遵循 BCP 47 和 Unicode CLDR 標準,支援 ICU Message Format,並支援日期、時間和數字等的國際化。

React Intl 通過元件的形式實現多語言:

<FormattedMessage
  id="welcome"
  defaultMessage={`Hello {name}, you have {unreadCount, number} {unreadCount, plural,
    one {message}
    other {messages}
  }`}
  values={{name: <b>{name}</b>, unreadCount}}
/>
複製程式碼

更具體的使用方式可以參考它的 Github:github.com/yahoo/react…

Angular 的方案

Angular 應該是目前主流前端框架中唯一自帶 i18n 解決方案的框架,它同樣遵循 BCP 47 和 Unicode CLDR 標準,支援 ICU Message Format。

與一般的 i18n 方案不同,Angular 不需要提前準備一份 JSON 或其它形式的多語言對映表,只需使用 i18n 屬性來標記需要進行多語言的文字即可,例如:

<h1 i18n>Hello, webnovel</h1>
複製程式碼

通過執行 ng xi18n 命令,Angular 會自動提取所有含義 i18n 屬性的字元,並生成一份 xlf 檔案(xlf 是一種基於XML的交換格式,旨在標準化本地化過程中在工具之間傳遞可本地化資料的方式),我們可以直接將 xlf 檔案傳送給譯者,譯者通過一些專門的軟體進行翻譯然後將這份檔案返回給我們。最後,我們通過預編譯或者即時編譯的方式將多語言內容注入到應用當中即可完成全部工作。

Angular 的方案非常完善,對我們可能不太注意的一些地方也做了支援,如我們想對 img 標籤的 title 屬性進行多語言的話, 只需再加上 i18n-title 的屬性即可,例如: <img [src]="logo" i18n-title title="Webnovel logo" />

Webnovel 的方案

Webnovel 的多語言版本

對比了上述的幾種方案,我們認為目前 Angular 的方案是最為理想。它相對完善;沒有很強的入侵性;得益於它能自動提取所需的字串並生成 ID,使用它的便利程度也優於其他框架;它的多語言支援預編譯和即時編譯,在效能和靈活度上都有了保證。

但比較遺憾的是 Webnovel 目前沒有使用 Angular,我們的移動站點和 App 分別基於 React 和 React Native 構建,由於已有的基於 react 的多語言庫 react-intl 不能很好的滿足我們的需要,我們決定自己構建 i18n 的基礎庫,它需要做到:

  • 能夠在常用 JavaScript 環境中使用,包括瀏覽器、 Node(服務端渲染)和 React Native
  • 遵循國際標準,支援 ICU MessageFormat
  • 支援字元模版的預編譯和即時編譯
  • 高效能,支援動態載入語言
  • 支援語言/字串的 fallback
  • 使用成本低
  • 外掛化

在有了基本目標以後,我們開發了新的多語言庫 react-i18n:

@react-i18n/core

核心庫,通過 React 的 Context API,提供 withI18n 的高階元件以及 Message 元件來幫助應用進行多語言。其中 withI18n 將 i18n 資訊傳遞給元件的 props,而 Message 元件類似於 React Intl 中的 FormattedMessage 元件,通過傳遞對應字串的 id 和引數來直接渲染出字元。

@react-i18n/cli

命令列工具,主要提供預編譯字元模版和一些輔助功能,如

  • Excel 處理:將 Excel 轉化為 JSON。我們和翻譯是通過線上的 Excel 來進行資訊同步。
  • 自動生成 ID:如 “hello world” 將生成 HELLO_WORLD_6f5902ac237024bdd0c176cb93063dc4 的 ID,既保證了在使用編輯器是能夠通過自動補全方便輸入,也保證了唯一性。
  • 機器翻譯:通過使用 Microsoft Translator Text API 和 Google Translation API 將機器翻譯的結果直接填入表格,幫助譯者更快的進行翻譯。

react-i18n 庫已經滿足了我們的基本要求,並且已經在 Webnovel 的移動站點和 App 中執行了一段時間。由於時間倉促, react-i18n 庫還不夠健全,我們暫時還不能將其開源。接下來,我們會將這個庫進行完善,儘早回饋給開源社群。

其他需要注意的問題

遵循已有標準

遵循已有標準對於國際化來說非常重要。我們在寫一個應用時,幾乎無法避免與第三方合作,例如支付。在合作的過程中,我們如果使用相同的標準,那溝通、除錯的成本將大大降低。關於國際化的標準非常多,有些也比較複雜,需要我們耐心認真的閱讀。值得注意的是,標準並不是一直不變的,例如上文中提到的 BCP 47 標準當中的地區標示,舊版中印尼的程式碼是 in ,而新版中改為了 id,一般我們應該遵循更新的標準。

更早、更多的瞭解成熟方案

對於國際化這樣已經存在非常多年的問題,一定是有成熟方案可以借鑑的,在開始之前,去儘可能多的瞭解已有方案,會讓我們少走許多彎路。

結語

本文重點講述了基於 Web 技術的一些國際化方案以及在 Webnovel 中的實際應用。國際化一直以來都是一個非常困難的問題,所以更需要我們長遠的去思考;國際化也不僅僅是多語言,從排版佈局到文化差異,都是我們需要考慮的,Webnovel 的國際化才剛剛開始。

相關連結

檢視更多分享,請關注閱文集團前端團隊公眾號:

Webnovel 國際化實踐

相關文章