介紹 JavaScript 國際化 API

jopen 10年前發布 | 30K 次閱讀 JavaScript開發 JavaScript

介紹 JavaScript 國際化 API

Firefox 29 是在半年之前發布的,所以這篇文章有點過時了。這里我先停一會,討論一下該桌面版附帶的國際化API(已經通過所有得測試)。 大多數的實現是由Norbert Lindenberg 所寫,由我審查和維護。(不久Makoto Kato的工作就會將這些帶到安卓中去;因為b2g的特殊困難,這可能要花挺長時間。現在仍在調整中。)

什么是國際化?

國際化(Internationalization的縮寫是i18n——i,中間18個字符,n)是將軟件處理的能讓來自各種地方使用各種語言的用戶更簡單使用的一個過程。假定某個用戶來自某個地方說某種語言,他可能不經意間就得到一些錯誤提示。尤其是你甚至都沒有做這種假設。

function formatDate(d)
{
  // Everyone uses month/date/year...right?
  var month = d.getMonth() + 1;
  var date = d.getDate();
  var year = d.getFullYear();
  return month + "/" + date + "/" + year;
}
 
function formatMoney(amount)
{
  // All money is dollars with two fractional digits...right?
  return "$" + amount.toFixed(2);
}
 
function sortNames(names)
{
  function sortAlphabetically(a, b)
  {
    var left = a.toLowerCase(), right = b.toLowerCase();
    if (left > right)
      return 1;
    if (left === right)
      return 0;
    return -1;
  }
 
  // Names always sort alphabetically...right?
  names.sort(sortAlphabetically);
}

JavaScript過去的i18n支持太糟糕

傳統JS的i18n程序使用toLocaleString()方法進行格式化。結果字符串包含實現自身提供的所有細節:沒有辦法自己選擇(你確實需要那種date格式的weekday嗎?year是無關緊要的嗎?)。即使包含對應的細節,格式也可能是錯的,比如本期望是百分比但得到的是數字。而且你還不能選擇一個區域設置(locale)。

對于排序,JS提供了基本沒用的基于區域設置(locale-sensitive)的文本比較函數。localeCompare()確實存在,但是其接口根本不適合sort。而且還不允許選擇區域設置,或者排序方式。

這些限制太糟了(當我認識到時,我非常吃驚!),因為需要i18n支持(通常是金融站點用于顯示貨幣)的嚴謹web應用會把數據打包,發給服務器,服務器進行操作,然后發回客戶端。數據往返服務器僅僅為了處理貨幣的數量。Yeesh。

新的JS國際化API

新的ECMAScript國際化API大大提升了JS的i18n能力。 它提供了大家所能想到的格式化date、數字,文本排序的方式。區域設置是可選的,如果請求的區域設置不支持可以回退。格式化請求可以指定具體要包含的組件。支持自定義的百分比、有效數字、貨幣格式。開放了大量排序選項用于文本排序。如果你關心性能,首要的操作是選擇一個區域設置,然后處理選項參數,現在這個操作只會處理一次,而不是之前每次區域設置相關的操作執行時都會被執行一遍。

這不是說,這個API是萬能藥,而僅僅是"盡最大努力"。精確的輸出幾乎總是故意不指定的。一份實現可以僅支持 oj 區域設置(合法的),也可以忽略(幾乎全部)提供的格式化選項。大多數實現都包含高質量的多區域支持,但并不保證有(尤其是資源限定的系統,如手機)。

在底層,Firefox的實現依賴于 Unicode 的國際化組件庫(ICU),這個庫又依賴 Unicode Common 區域數據倉庫(CLDR)的區域數據集。我們的實現是自托管的:ICU之上的大部分實現用JS寫的。在這個過程中,我們遇到了一些問題(我們從未如此大規模的自托管過),但基本上都不大。

Intl 接口(不是數字1,是字母l)

i18n 存在于 Intl 對象之上。Intl 包含3個構造函數:Intl.Collator, Intl.DateTimeFormat, 和Intl.NumberFormat。每個構造函數創建一個對象,這個對象提供相關操作、高效地為這些操作緩存區域設置和選項。按以下方式創建對象:

var ctor = "Collator"; // 或其他 
var instance = new Intl[ctor](locales, options);

locales 是個字符串,指定單個語言標簽,或者包含多個語言標簽的類數組對象。語言標簽如下面的字符串:en(普通英語),de-AT(奧地利德語),zh-Hant-TW(臺灣使用的繁體中文)。語言標簽可以包含一個“Unicode擴展”,形式為-u-key1-value1-key2-value2..., 其中每個key是“擴展key”。不同的構造函數對此進行具體解釋。

opions 是個對象,其屬性(如果不存在,就賦值為undefined)決定格式化器(formatter)和整理器(collator)的行為。精確的解釋由構造函數決定。

給定區域信息和選項,實現會嘗試生成近似理想行為的最接近行為。Firefox 支持用于整理(collation)的400+區域,用于date/time和數字格式化的600+區域,所以很可能(但不保證)你想要的區域是被支持的。

Intl 通常不保證某些特定行為。如果請求的區域不被支持,Intl 允諾“盡最大努力”的行為。即使區域是被支持的,行為也不是嚴格指定的。永遠不要假設特定的選項集適用于某個特定格式。(圍繞請求的組件)總體格式的用語可能因瀏覽器甚至瀏覽器的版本而不同。單個組件的格式是未指定的:weekday的短格式可以為“S”, “Sa”, 或“Sat”。Intl API并不用于公開精確的特定行為。

Date/time格式化

選項

date/time格式化的主要選項屬性如下:

  • weekday, era
    "narrow", "short", or "long".  (era通常指歷法系統中長于一年的分段,如現行日皇統治, 或者其他紀年法)

  • month
    "2-digit", "numeric", "narrow", "short", or "long"

  • year

  • day

  • hour, minute, second
    "2-digit" or "numeric"

  • timeZoneName
    "short" or "long"

  • timeZone
    區分大小寫的"UTC"通過對應的toUTC進行格式化。有些值如"CEST"和"America/New_York"不是必須被支持的,它們確實在當前Firefox下沒有效果。 

這些值并不映射到特定格式:記住Intl API幾乎不指定精確的行為。Intl的目的,舉例來說是"narrow", "short", 和"long"生成對應大小的“S”/“Sa”, “Sat”, 和“Saturday”(輸出可能不太準確,因為Saturday和Sunday都可以生成“S”)。  "2-digit"和"numeric"映射到2位數字的字符串或者全長度的數字字符串,如“70”和“1970”。

最終使用的選項大部分是請求的選項。但是,如果你不指定請求的 weekday/year/month/day/hour/minute/second,那么 year/month/day 將會被加入到你提供的選項。

除此之外,還有些特殊的選項:

  • hour12
    指定hour采用12小時還是24小時格式。默認通常是依賴于區域設置的(某些細節,如午夜是0點,還是12點,以及是否存在前導0,都是依賴于區域設置的)。

還有另外2種特殊屬性,localeMatcher (可選"lookup"或"best fit") 和formatMatcher (可選"basic"或"best fit"),兩者默認值都是"best fit"。這些會影響正確的區域設置和格式的選取。它們的用例可能比較難懂,就不贅述了。

區域設置相關選項

DateTimeFormat也允許通過自定義歷法和數字系統來格式化。具體細節存在于區域設置,所以它們可以在語言標簽的Unicode擴展中找到。

例如,泰國的泰語中語言標簽為th-TH。回一下Unicode擴展的格式-u-key1-value1-key2-value2....  歷法系統的key是ca, 數字系統的key時nu。泰語數字系統值為thai,中文歷法系統值為chinese。因此用大體這樣的方式來格式化date,我們把包含這些key/value對的Unicode追加到語言標簽上去:th-TH-u-ca-chinese-nu-thai。

關于更多歷法和數字系統的信息,查看DateTimeFormat的完整文檔

舉例

創建DateTimeFormat對象后,下一步是通過方便的format()函數來格式化date。更方便的是,這個函數是有界函數(bound function):你不必在DateTimeFormat上直接調用。之后給它傳遞一個時間戳或者Date對象。

總結一下,下文是如何為特定用途創建DateTimeFormat選項的例子(在當前Firefox執行行為下)。

var msPerDay = 24 * 60 * 60 * 1000; 

// July 17, 2014 00:00:00 UTC.
var july172014 = new Date(msPerDay * (44 * 365 + 11 + 197));

我們來格式化美國英語的date。我們先創建一個2位數字的month/day/year, 加上2位數字的hours/minutes, 還有一個短時區來確定這個時間。(結果肯定因不同時區而明顯不同)

var options =
  { year: "2-digit", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit",
    timeZoneName: "short" };
var americanDateTime =
  new Intl.DateTimeFormat("en-US", options).format; 

print(americanDateTime(july172014)); // 07/16/14, 5:00 PM PDT

或者類似的,對葡萄牙語,最好是巴西使用的葡語,但是用在葡萄牙作品中的。格式會稍長,因為包含完整的year和正式拼寫的month,但是因移植性要轉成UTC格式。

var options =
  { year: "numeric", month: "long", day: "numeric",
    hour: "2-digit", minute: "2-digit",
    timeZoneName: "short", timeZone: "UTC" };
var portugueseTime =
  new Intl.DateTimeFormat(["pt-BR", "pt-PT"], options); 

// 17 de julho de 2014 00:00 GMT
print(portugueseTime.format(july172014));

那對于一個壓縮的,UTC格式的瑞士火車每周調度表?我們嘗試用正式語言按流行度從大到小來選擇最易讀的一個。

var swissLocales = ["de-CH", "fr-CH", "it-CH", "rm-CH"];var options =
  { weekday: "short",
    hour: "numeric", minute: "numeric",
    timeZone: "UTC", timeZoneName: "short" };
var swissTime =
  new Intl.DateTimeFormat(swissLocales, options).format; 

print(swissTime(july172014)); // Do. 00:00 GMT

或者我們嘗試某個日本博物館里的一幅畫中的描述性文本中的date,這個date使用日本的year和era歷法。

var jpYearEra =
  new Intl.DateTimeFormat("ja-JP-u-ca-japanese",
                          { year: "numeric", era: "long" }); 

print(jpYearEra.format(july172014)); // 平成26年

對一些完全不同的、更長的date,用于泰國泰語,但是使用泰語數字系統和中國歷法。(類似Firefox的高質量實現通常會將普通的th-TH當做th-TH-u-ca-buddhist-nu-latn, 因為泰國使用佛歷系統和拉丁0-9數字)。

var options =
  { year: "numeric", month: "long", day: "numeric" };
var thaiDate =
  new Intl.DateTimeFormat("th-TH-u-nu-thai-ca-chinese", options); 

print(thaiDate.format(july172014)); // ?? 6 ??

撇開歷法和數字系統,還是很簡單的。只要選取自己的組件和長度。

數字格式化

選項

數字格式化的主要選項屬性有:

  • style
    "currency", "percent", or "decimal" (默認值).

  • currency
    3字母貨幣代碼,如USD、CHF。需要style是"currency", 不然沒有意義

  • currencyDisplay
    "code", "symbol", or "name", 默認為"symbol".  "code"使用格式字符串的3字母貨幣代碼。"symbol"使用貨幣符號,如$或£。"name"通常使用某些正式拼寫版本的貨幣。(Firefox 目前僅支持"symbol", 這個問題不就就修復)

  • minimumIntegerDigits
    范圍1到21(包含)的整數,默認為1。結果字符串的整數部分如果沒有這么長,它前面會用0來填充。  (若這個值為2,那么3的格式化形式為“03”。)

  • minimumFractionDigits, maximumFractionDigits
    0-20(包含)的整數。結果字符串至少minimumFractionDigits, 至多maximumFractionDigits個有效數字。如果style是"currency",默認最小值跟貨幣有關(通常是2,很少0或者3),不然就是0。默認最大值,百分比是0,數字是3,貨幣的最大值跟貨幣有關。

  • minimumSignificantDigits, maximumSignificantDigits
    1-21(包含)的整數。如果有,它們將會覆蓋上文關于整數/分數的對數字的控制,而由它們以及數字的要求長度,共同確定格式化的數字字符串中最小/最大有效數字的值。 (注意對10的倍數,有效數字可能不準確,如100,它的1,2,3位有效數字。)

  • useGrouping
    布爾值(默認true)決定格式化字符串是否包含分組分隔符(如,英語的千分隔符“,”)。

NumberFormat還識別難懂的、大多數可忽略的localeMatcher屬性。

區域化選項

在Unicode擴展中,使用nu關鍵字可以使DateTimeFormat支持自定義數字系統,NumberFormat也是這樣。 例如,在中國,中文的語言標簽是zh-CN。 漢語十進制數字系統對應的值是hanidec。 為了格式化這些系統的數字,我們在這些語言標簽上添加一些Unicode擴展:zh-CN-u-nu-hanidec。

關于不同數字系統標識的完整信息,見NumberFormat詳細說明文檔

示例

NumberFormat對象有一個 format方法,這一點和 DateTimeFormat相同。 format方法是一個有界函數,它有時可以獨立于 NumberFormat使用。

下面是在當前Firefox環境下為特定用途創建NumberFormat選項的例子。首先,我們來格式化中國大陸中文的貨幣格式,特別是使用漢字數字(而不是更普遍的拉丁數字)。選擇"currency" style, 然后使用renminbi(yuan)的貨幣代碼,數字分組默認,小數部分數字采用通常做法。

var tibetanRMBInChina =
  new Intl.NumberFormat("zh-CN-u-nu-hanidec",
                        { style: "currency", currency: "CNY" }); 
print(tibetanRMBInChina.format(1314.25)); // ¥ 一,三一四.二五

或者我們來格式化美國的天然氣價格,千分位有個古怪的9,用在美國英語中。

var gasPrice =
  new Intl.NumberFormat("en-US",
                        { style: "currency", currency: "USD",
                          minimumFractionDigits: 3 }); 
print(gasPrice.format(5.259)); // $5.259

或者我們嘗試埃及的阿拉伯語中的百分比。確定百分比有至少2個有效數字。(注意這個以及其他RTL例子可能在RTL上下文出現的順序不一樣,如?????? 而不是??????,RTL/right to left,從右到左的)

var arabicPercent =
  new Intl.NumberFormat("ar-EG",
                        { style: "percent",
                          minimumFractionDigits: 2 }).format; 
print(arabicPercent(0.438)); // ??????

或者假設我們格式化阿富汗波斯語,我們期望至少2位的整數部分,至多2位的小數部分。

var persianDecimal =
  new Intl.NumberFormat("fa-AF",
                        { minimumIntegerDigits: 2,
                          maximumFractionDigits: 2 }); 
print(persianDecimal.format(3.1416)); // ?????

最后,我們格式化巴林的阿拉伯語中的巴林第納爾的數量。不同于大部分貨幣,巴林第納爾等于1000費爾,因此我們需要三位小數位。(再次注意,不要太過相信表面的閱讀順序)?

var bahrainiDinars =
  new Intl.NumberFormat("ar-BH",
                        { style: "currency", currency: "BHD" }); 
print(bahrainiDinars.format(3.17)); // ?.?.? ?????

整理

選項

下面是整理的主要選項屬性:

  • usage
    "sort" or "search" (默認"sort"), 指定整理器(collator)的目的。(查找整理器可能比排序整理器更多考慮字符串是否相等。)

  • sensitivity/敏感性
    "base", "accent", "case", or "variant"。這個選項影響整理器對基本字符相同但是重音/變音不同的字符的敏感性。(基本字符與區域設置有關:“a”和“?”在德語中基本字符相同,但是瑞典語中是不同的。)  "base"敏感性只考慮基本字符,忽略各種變體(如德語中“a”, “A”,和“?”被認為是相同的)。 "accent"敏感性考慮基本字符和重音,但是忽略大小寫(如德語中的“a和“A”是相同的,“?”與兩者不同)。"case"考慮基本字符和大小寫,而忽略重音(如德語中的“a”和“?”相同,而與“A”不同)。"variant"考慮基本字符、重音、大小寫(如德語的“a”, “?”和“A”均不同)。如果usage是"sort",默認"variant"; 否則與區域設置有關。

  • numeric
    默認false的布爾值,決定字符串中的數字是否被當做數字參與比較。如當做數字的排序結果可能是"F-4 Phantom II", "F-14 Tomcat", "F-35 Lightning II"; 不當做數字的結果"F-14 Tomcat", "F-35 Lightning II", "F-4 Phantom II".

  • caseFirst
    "upper", "lower", or "false" (默認)。決定比較時是否考慮大小寫:"upper"把大寫放在前面("B", "a", "c"), "lower"把小寫放前面("a", "c", "B"), "false"完全忽略大小寫("a", "B", "c")。 (注意,目前Firefox完全忽略這個屬性)

  • ignorePunctuation
    默認false的布爾值。決定比較時是否忽略標點(如"biweekly"和"bi-weekly"是否相等)。

localeMatcher屬性你可以忽略了。

區域相關選項

區域的Unicode擴展部分指定的整理器主要選項是co,它選擇排序操作的方式:電話本(phonebk), 字典 (dict), 還有其他

另外,kn和kf這兩個key可以選擇拷貝選項對象的numeric和caseFirst屬性。但是并不保證可用于語言標簽,而且選項比語言標簽的組件更清晰。所以最好在選項內部進行調整。

key-value對(paris)嵌入到Unicode的方式跟DateTimeFormat和NumberFormat相同; 想知道如何在語言標簽中指定它們,可以查看對應的章節。

舉例

整理器Collator對象有個比較函數屬性。這個函數接受2個參數x和y, 如果x<y返回負值,x>y返回正值,x==y返回0。對于格式化函數,比較是個有界函數(bound function),可以抽取出來做其他用途。

我們嘗試來給德國德語的姓氏進行排序。德語中實際上有2種排序方式,電話本和字典。電話本排序強調讀音,比如“?”, “?”等近似被擴展成“ae”, “oe”等。

var names =
  ["Hochberg", "H?nigswald", "Holzman"]; 
var germanPhonebook = new Intl.Collator("de-DE-u-co-phonebk"); 

//就像對["Hochberg", "Hoenigswald", "Holzman"]排序 
//   Hochberg, H?nigswald, Holzman
print(names.sort(germanPhonebook.compare).join(", "));

有些德語單詞使用變音符進行詞形變化,所以在字典中排序會忽略變音符(除了兩個單詞僅僅變音符不同:schonsch?n)。

var germanDictionary = new Intl.Collator("de-DE-u-co-dict");  

//就像對["Hochberg", "Hoenigswald", "Holzman"]排序 
//   Hochberg, H?nigswald, Holzman
print(names.sort(germanDictionary.compare).join(", "));

或者我們來對美國英語的Firefox的版本進行排序,這些字符串故意打錯(大小寫不同,隨機重音,變音標記,額外的連字符)。我們希望根據版本號進行排序,所以要做numeric排序,這樣字符串中的數字才會作為數字參與比較,而不是逐字符的。

var firefoxen =
  ["FireF?x 3.6",
   "Fire-fox 1.0",
   "Firefox 29",
   "Fírefox 3.5",
   "Fírefox 18"]; 
var usVersion =
  new Intl.Collator("en-US",
                    { sensitivity: "base",
                      numeric: true,
                      ignorePunctuation: true }); 

// Fire-fox 1.0, Fírefox 3.5, FireF?x 3.6, Fírefox 18, Firefox 29
print(firefoxen.sort(usVersion.compare).join(", "));

最后,我們來做跟區域有關的字符串查找,查找忽略大小寫和重音,并用于美國英語。

// Comparisons work with both composed and decomposed forms.
var decoratedBrowsers =
  [
   "A\u0362maya",  // A?maya
   "CH\u035Br?me", // CH?r?me
   "Firefóx",
   "sAfàri",
   "o\u0323pERA",  // ?pERA
   "I\u0352E",     // I?E
  ]; 
var fuzzySearch =
  new Intl.Collator("en-US",
                    { usage: "search", sensitivity: "base" }); 
function findBrowser(browser)
{
  function cmp(other)
  {
    return fuzzySearch.compare(browser, other) === 0;
  }
  return cmp; 
}  
print(decoratedBrowsers.findIndex(findBrowser("Firêfox"))); // 2 
print(decoratedBrowsers.findIndex(findBrowser("Saf?ri")));  // 3 
print(decoratedBrowsers.findIndex(findBrowser("?maya")));   // 0 
print(decoratedBrowsers.findIndex(findBrowser("?pera")));   // 4 
print(decoratedBrowsers.findIndex(findBrowser("Chromè")));  // 1 
print(decoratedBrowsers.findIndex(findBrowser("I?")));      // 5

瑣碎

檢測某個操作是否支持特定區域,或者一個區域是否被支持,會很有用。Intl在每個構造函數上都提供了supportedLocales()函數,在每個原型上提供了resolvedOptions()函數來公開這些信息。

var navajoLocales =
  Intl.Collator.supportedLocalesOf(["nv"], { usage: "sort" });
print(navajoLocales.length > 0
      ? "Navajo collation supported"
      : "Navajo collation not supported"); 
var germanFakeRegion =
  new Intl.DateTimeFormat("de-XX", { timeZone: "UTC" });
var usedOptions = germanFakeRegion.resolvedOptions();
print(usedOptions.locale);   // de
print(usedOptions.timeZone); // UTC

遺留行為

ES5的toLocaleString和localeCompar函數之前沒有特定語義,不接受特定選項,基本上沒有任何用處。因此i18n API根據Intl操作重組了它們。現在每個方法都接受附加的尾隨locales和options參數,它們會像Intl構造函數一樣被解釋。(除了toLocaleTimeString 和 toLocaleDateString, 如果options沒有,它們就會使用不同的默認組件)

對并不關心精確行為的簡單應用,這沒關系,老方法也可以用。但是如果需要更多控制或者多次格式化、比較的話,最好直接使用Intl。

結論

國際化是個非常有意思的話題,它的復雜性僅在于人們溝通方式的多樣性。國際化API解釋了這個復雜性的一小部分但是非常有用的部分,使創建區域設置有關的web應用更加方便。所以趕緊使用吧!

(非常感謝Norbert Lindenberg, Anas El Husseini, Simon Montagu, Gary Kwong, Shu-yu Guo, Ehsan Akhgari, #mozilla.de的成員, 還有我可能忘掉的人[sorry!], 他們對本文提供了反饋或者幫助我編寫或批評修正例子。英語和德語的例子已經超過了我的能力,對于其他例子,如果沒有他們,我已經不知道怎么做了。如果還有錯誤,都責怪我吧。再次感謝!)

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!