馬什麼梅?I什麼N?淺談 web 前端開發中的國際化

江米小棗tonylua發表於2018-10-11

馬什麼梅?I什麼N?淺談 web 前端開發中的國際化

I. 國際化、本地化、全球化

很多開發者會有這樣的經歷,在若干年之前,一些企業、機構、學校的官方網站會要求:“翻譯一下,也做個英文版的”,結果往往就是又單獨維護一套英文版的頁面。

而在當今的軟體開發領域,隨著越來越多的產品需要真正面向海外市場售賣的情況,以前那種簡單粗暴的做法就不適用了;這就需要靈活的修改軟體,使之能適應目標市場的語言、地區差異以及技術需要

本文嘗試對相關概念,特別是在 web 前端領域的應用,做出簡單介紹。

1.1 術語

提起國際化時,也常常牽扯出幾個相似的術語:

  • [國際化]:internationalization,因首尾字母間有 18 個字母,簡稱為 i18n;指的是將軟體與特定語言及地區脫鉤的過程。當軟體被移植到不同的語言及地區時,軟體本身不用做內部工程上的改變或修正
  • [本地化]:localization,由於同樣的原因被簡稱為 l10n;是指為特定區域翻譯檔案,併為了使軟體能在該特定語言環境或地區使用,而應用特殊佈局、加入本地特殊化部件等的過程
  • [全球化]:globalization,有時會用來表示以上兩者的合稱;也會簡稱為 g11n

簡單來說:國際化搭臺、本地化唱戲。國際化意味著產品有適用於任何地方的“潛力”;本地化則是為了更適合於“特定”地方的使用,而另外增添的特色。用一項產品來說,國際化只需做一次,但本地化則要針對不同的區域各做一次。兩者之間是互補的,並且合起來才能讓一個系統適用於各地。

1.2 國際化的特徵

一個經過國際化的軟體具備如下特徵:

  • 可以快速的本地化
  • 藉助本地化的資料,相同的程式可以執行在世界各地
  • 諸如狀態提示、介面中的標題等文字元素,並非硬編碼到軟體中,而是儲存到原始碼之外,且能動態改變
  • 不需要重新編譯軟體就能新增新的語言
  • 貨幣、日期等資訊的表現形式,適應於使用者所在的地區和語言

1.3 需要本地化的元素

除了以上提到的貨幣和日期,還有很多元素與文化、地域、語言相關,比如:

  • 書寫方向
  • 聲音
  • 顏色
  • 圖形
  • 圖示
  • 時間
  • 數字
  • 度量
  • 電話號碼
  • 敬語
  • 頭銜
  • 郵政地址
  • 分頁
  • 排序方法
  • 輸入處理
  • 方言
  • 法規
  • 道德和習慣

II. 區域設定

除了 i18nl10ng11n 等這些奇怪的叫法,我們日常也經常見到 locale 這個詞,其取值一般為 zh-CNen-US 等。

一個 locale 物件就是個結合了語言和地區的特殊識別符號

locale 由"網際網路工程任務組"(IETF)的“BCP 47” 文件系列(tools.ietf.org/html/bcp47) 定義。

常見的典型形式 由分別表示語言和地區的兩部分組成,用 - 連線;也可以省略地區。

舉例來說:

locale code 通常的含義
en 英語
en-US 美國講的英語
en-GB 英國講的英語
zh-CN 簡體中文
zh-HK 香港地區繁體中文
zh-TW 臺灣地區繁體中文
zh-SG 新加坡簡體中文
zh-MO 澳門地區繁體中文
es-AR 阿根廷講的西班牙語
ar-001 通用阿拉伯語
ar-AE 阿聯酋講的阿拉伯語

2.1 區域敏感

一般來說,根據 locale 的設定,把一個 hello 分別翻譯成 “こんにちは” 或 “你好” 就可以了;但涉及數字、日期、貨幣等,需要特殊的格式,或計算年號等,可以用一些專用的類來處理。

如果程式碼的行為取決於 locale,則說它是“區域敏感的(locale-sensitive)”

比如,Java 中的 NumberFormat 類就是區域敏感的;其數字返回值的格式取決於 locale:可能為 902 300 (法國),或 902.300 (德國),又或者 902,300 (美國)。

當然,類似的類現在也出現在了 JS 中,相關內容會在後面提及。

需要注意的是,locale 就只是個識別符號,而識別單詞邊界、格式化等具體的工作,還是都需要由區域敏感的程式碼模組來完成的。

2.2 語言編碼

locale 的前半部分表示語言,通常由 2 或 3 位小寫字母組成,符合 ISO 639www.loc.gov/standards/i… 標準。

例如:

Language Code Description
de German
en English
fr French
ru Russian
ja Japanese
jv Javanese
ko Korean
zh Chinese

2.3 地區編碼

locale 的後半部分表示地區,由符合 ISO 3166www.chemie.fu-berlin.de/diverse/doc… 標準的 2 或 3 位大寫字母,或符合 UN M.49 標準的 3 位數字組成。這部分是可選的。

例如:

A-2 Code A-3 Code Numeric Code Description
AU AUS 036 Australia
BR BRA 076 Brazil
CA CAN 124 Canada
CN CHN 156 China
DE DEU 276 Germany
FR FRA 250 France
IN IND 356 India
RU RUS 643 Russian Federation
US USA 840 United States

2.4 較少用到的更完整定義

這部分內容很少用到,權作了解即可,可以參閱文末的 wiki 連結。

語言標籤由一個或多個子標籤(subtags)組成,用連字號 - 分隔。子標籤只能由基本拉丁字母或數字組成

在 BCP 47 語言標記標準中,完整的子標籤依次為:

子標籤 是否必須 是否常見 解釋
language 見 2.1
extended language 最多3個,每個由3字母組成。實際上還沒有使用
script 一般 4個字母,首字母大寫
region 見 2.2
variant 近一步區分 language 無法覆蓋的方言;為5至8個字母,或4字母后跟1個數字
extension 一般 比如以 u 開頭,後跟連字元與2至8個字元組成的文字(),用來表示 unicode,會影響 NumberFormat 等地區敏感的國際化模組,具體見下方例子
private-use x- 字首開頭的私用語言標籤

最基礎的情況舉例如下:

  • hi:印地語
  • de-AT:在奧地利使用的德語
  • zh-Hans-CN:在中國使用的中文簡體
  • zh-Hant-HK:在中國香港使用的中文繁體
  • zh-Hans:同時包含了 zh-CNzh-SG(新加坡)等使用簡體字的地區

而關於 extension 的部分,以這個例子直觀的看一下:

var date = new Date(1945,7,15);

function printDate(locale) {
    var dtf = new Intl.DateTimeFormat(
        locale,
        {era:"short"} //一個 options 物件,還有很多引數可設定
    );
    console.log( dtf.format(date) );
}

printDate("en"); //8 15, 1945 AD
printDate('ja-JP-u-ca-japanese'); //昭和20年8月15日
printDate('zh-Hant-u-ca-buddhist'); //佛曆2488年8月15日
printDate('zh-Hans-u-nu-thai-ca-japanese'); //昭和๒๐年๘月๑๕日
printDate("zh-Hans-u-nu-fullwide-ca-persian"); //波斯歷1324年5月24日
printDate('zh-T' + 'W-u-ca-r' + 'oc'); //這個可以自己試一下~
複製程式碼

簡單說其形式就是 u- 後面自由組合 nu-ca- 兩個子部分,分別表示 number 和 calendar 遵從何種語言。具體可用值可以檢視 MDN 上 Intl.DateTimeFormat 的頁面。

馬什麼梅?I什麼N?淺談 web 前端開發中的國際化
#為何要學好普通話#: 天不怕,地不怕,就怕南方人說兒化音

III. 字元編碼

現在做 web 前端開發,隨著工具、框架的完善和標準的普及,已經較少出現“中文亂碼”的情況,但這曾經是前後端都經常遇到的情況;這時就要了解字元編碼的問題,在一些需要國際化的情況下也要做相應的轉換或宣告。

計算機在設計之初,並沒有考慮多個國家、多種不同語言的應用場景。當時定義一種 ASCII 碼,將字母、數字和其他符號編號用 7 位元的二進位制數來表示。後來,計算機在世界範圍內開始普及,為了適應多種文字,出現了多種編碼格式,例如中文漢字一般使用的編碼格式為 GB2312GBK

由此,又產生了一個問題,不同字元編碼之間互相無法識別。於是出現了 Unicode 編碼。它為每種語言的每個字元設定了統一併且唯一的二進位制編碼。

Unicode 也有一個缺點:為了支援所有語言的字元,所以它需要用更多位數去表示,比如 ASCII 表示一個英文字元只需要一個位元組,而 Unicode 則需要兩個位元組。很明顯,字元數多,效率會很低。

為了解決這個問題,由出現了一些中間格式的字元編碼:如常見的 UTF-8,以及 UTF-16UTF-32 等。

IV. Java 中的國際化

發黃的相片
古老的信
以及褪色的聖誕卡
年輕時為你寫的...Java 程式碼裡
已經有了成熟的 i18n 方案
複製程式碼

之所以要在這裡提一下“古舊的後端語言” -- Java 中的國際化,是因為其解決方案對後來的 jQuery、Vue.js 等工具/框架的國際化方案,以及新的 ECMAScript Internationalization API 都產生了深刻的影響。

4.1 資原始檔

Java 將不同語言的文字儲存在字尾為 .properties 的檔案中,其格式為
<資源名>_<語言程式碼>_<國家/地區編碼>.properties

其中,語言編碼和國家/地區編碼都是可選的;以 <資源名>.properties 命名的國際化資原始檔是預設的資原始檔。

注意這裡是用 _ 做的連線,和標準中規定的 - 稍有不同。

# MessagesBundle.properties
greetings = Hello.
farewell = Goodbye.
inquiry = How are you?
複製程式碼
# MessagesBundle_zh_CN.properties
greetings = 嗨!
farewell = 再見!
inquiry = 吃了沒?
複製程式碼

注:如果是用 ASCII 編碼的中文資原始檔,還需要用 native2ascii 轉成 Unicode 編碼

4.2 選擇和載入語種

在 Java 中,用一個 java.util.Locale 物件選擇區域設定;而 java.util.ResourceBoundle 則用於載入本地化資原始檔。

import java.util.Locale;
import java.util.ResourceBundle;

public class I18NSample {

   static public void main(String[] args) {

      String language;
      String country;

      if (args.length != 2) {
          language = new String("en");
          country = new String("US");
      } else {
          language = new String(args[0]);
          country = new String(args[1]);
      }

      Locale currentLocale;
      ResourceBundle messages;

      currentLocale = new Locale(language, country);
      messages = ResourceBundle.getBundle("MessagesBundle",currentLocale);

      System.out.println(messages.getString("greetings"));
      System.out.println(messages.getString("inquiry"));
      System.out.println(messages.getString("farewell"));
      System.out.println("-----");
   }
}
複製程式碼

這段程式簡單易懂,根據執行引數選擇不同的語言包,並從中取出相應欄位。

javac I18NSample.java

java I18NSample
//Hello.
//How are you?
//Goodbye.

java I18NSample zh CN
//嗨!
//吃了沒?
//再見!
複製程式碼

4.3 國際化工具類

Java 中也提供了幾個區域敏感的國際化格式工具類。例如:NumberFormatDateFormatMessageFormat

import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Date;
import java.util.GregorianCalendar;
import java.text.NumberFormat;
import java.text.DateFormat;
import java.text.MessageFormat;

public class I18NSample2 {

   static public void main(String[] args) {

    double num = 123456.78;
    NumberFormat format = NumberFormat.getCurrencyInstance(Locale.SIMPLIFIED_CHINESE);
    System.out.format("%f 的本地化(%s)結果: %s\n", num, Locale.SIMPLIFIED_CHINESE, format.format(num));
    // 123456.780000 的本地化(zh_CN)結果: ¥123,456.78


    Date date = new Date();
    
    DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.JAPANESE);
    System.out.format("%s 的本地化(%s)結果: %s\n", date, Locale.JAPANESE, df.format(date));
    // Wed Oct 10 23:25:33 CST 2018 的本地化(ja)結果: 2018/10/10
    
    DateFormat df2 = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.SIMPLIFIED_CHINESE);
    System.out.format("%s 的本地化(%s)結果: %s\n", date, Locale.SIMPLIFIED_CHINESE, df2.format(date));
    // Wed Oct 10 23:25:33 CST 2018 的本地化(zh_CN)結果: 2018年10月10日

    
    Object[] params = {"Jack", new GregorianCalendar().getTime(), 8888};

    String pattern1 = "{0},你好!你於  {1} 消費  {2} 元。";
    String msg1 = MessageFormat.format(pattern1, params);
    System.out.println(msg1);
    // Jack,你好!你於  2018/10/10 下午11:25 消費  8,888 元。

    String pattern2 = "At {1,time,short} On {1,date,long},{0} paid {2,number, currency}.";
    MessageFormat mf = new MessageFormat(pattern2, Locale.US);
    String msg2 = mf.format(params);
    System.out.println(msg2);
    // At 11:25 PM On October 10, 2018,Jack paid $8,888.00.
   }
}
複製程式碼

格里高利曆(Gregorian calendar)是公曆的標準名稱,是一種源自於西方社會的歷法。因為由教皇格里高利十三世於 1582 年頒佈而得名。而公元即“公曆紀元”,又稱“西元”。 -- 百度百科

首先 GregorianCalendar 這個類的命名解釋如上,當然寫國際化程式碼時聽聽英國那個同名的 Gregorian 教皇合唱團也是極好的~

其次,在 MessageFormat 中,我們在訊息模版字串見到了一些佔位符。其規則大致為:

  1. 用傳入的實際值,依次按花括號中的數字進行替換
  2. 數字位置可以任意指定,同一個數字可以出現多次
  3. 花括號完整的格式為 { ArgumentIndex , FormatType , FormatStyle },後兩者可選
  4. FormatType 可以是 number、date、time 等
  5. FormatStyle 可以是 short、medium、long、full、integer、currency、percent 等

V. jQuery 中的國際化

作為早期推動 web 前端開發的最主要工具,jQuery 是一個時代當仁不讓的開發標配。

在處理國際化需求時,jQuery 本身並不包含相關模組,應用比較廣泛的一個第三方輕量化外掛是 jQuery.i18n.properties

5.1 jQuery.i18n.properties 的特點

  • 與 Java 裡的做法如出一轍的是,jQuery.i18n.properties 採用 .properties 檔案對 JavaScript 程式碼進行國際化。要在 Java 程式和前端 JavaScript 程式中共享資原始檔時,這種方式也提供了便利。
  • 資原始檔命名為 <資源名>_<語言程式碼>-<國家/地區編碼>.properties,其中語言和區域之間採用 - 連線,目的是為了相容瀏覽器由 navigator.languagejQuery.i18n.browserLang() 獲得的 locale 值
  • 可以在資源字串中使用佔位符(例如:hello= 你好 {0}! 今天是 {1}。)
  • 資原始檔中的 Key 支援名稱空間(例如:com.company.msgs.hello = Hello!)
  • 支援資原始檔中跨行的值
  • 支援以 JavaScript 變數(或函式)或 Map 的方式使用資原始檔中的 Key

5.2 基本用法

jQuery.i18n.properties({ 
   name:'strings',// 資原始檔名稱
   path:'bundle/',// 資原始檔所在目錄路徑
   mode:'both',// 模式:變數或 Map 
   language:'pt_PT',// 對應的語言
   cache:false, //瀏覽器是否對資原始檔進行快取
   encoding: 'UTF-8', //編碼。預設為 UTF-8
   callback: function() {// 回撥方法
   } 
});
複製程式碼

在 callback 回撥中,可以呼叫·jQuery.i18n.prop(key) 方法。該方法以 map 的方式使用資原始檔中的值,其中 key 指的是資原始檔中的 key。當 key 指定的值含有佔位符時,可以使用 jQuery.i18n.prop(key,var1,var2 … ) 的形式,其中 var1,var2 … 對各佔位符依次進行替換。

VI. Vue.js 中的國際化

隨著 Angular、React、Vue.js 具有指標性的開發工具相繼問世,這三駕馬車把前後端分離也帶入了新的時代,更多的開發邏輯從後端讓渡到前端來,相應的國際化需求也更為常見,對其靈活性易用性的要求也更高。

此處以 Vue.js 為例分析其常見的 i18n 解決方案。

vue-i18n (kazupon.github.io/vue-i18n/) 是較常用的一款 Vue.js 國際化外掛,其主要特性包括:

  • 各種格式的本地化
  • 支援複數等語言規則 (Pluralization)
  • DateTime 本地化
  • Number 本地化
  • 基於元件的本地化
  • 元件插值 (Component interpolation)
  • Fallback 本地化

6.1 基本用法

<div id="app">
  <p>{{ $t("message.hello") }}</p>
</div>
複製程式碼

import Vue from 'vue'
import VueI18n from 'vue-i18n'

// 註冊外掛
Vue.use(VueI18n)

// 多語言資訊
// 一般可以單獨模組化,放到 i18n.js 等檔案中
const messages = {
  en: {
    message: {
      hello: 'hello world'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、世界'
    }
  }
}

// 傳入一個 options,建立一個 VueI18n 例項
const i18n = new VueI18n({
  locale: 'ja', // 區域設定
  messages, 
})

// 在 Vue 的例項化選項中傳入 i18n
new Vue({ i18n }).$mount('#app')
複製程式碼

輸出:

<p>こんにちは、世界</p>
複製程式碼

可見,Vue.js 中不再依賴於 .properties 資原始檔,而是用 JS 自身的物件或模組化解決。

6.2 多語言資源語法

除了簡單的鍵值對,Vue.js 還支援多種靈活的語法:

{
  "en": {  // 英語
      "key1": "this is message1", // 基礎的鍵值對
      "nested": { // 巢狀的
        "message1": "this is nested message1"
      },
      "errors": [ // 陣列,裡面可以放各種值
        "this is 0 error code message",
        {"internal1": "this is internal 1 error message"},
        ["this is nested array error 1"]
      ],
      
      //常用詞語可以用來拼接
      "the_world": 'the world',
      "dio": 'DIO:',
      "linked": '@:message.dio @:message.the_world !!!!'
  },
  "ja": { // 日語
    // ...
  }
}
複製程式碼

<p>{{ $t('key1') }}</p>
<p>{{ $t('nested.message1') }}</p>
<p>{{ $t('errors[0]') }}</p>
<p>{{ $t('errors[1].internal1') }}</p>
<p>{{ $t('errors[2][0]') }}</p>

<p>{{ $t('message.linked') }}</p>
複製程式碼

6.3 佔位符

傳統的佔位符形式,此處被稱為“列表格式化”(List formatting):

<p>{{ $t('message.hello', ['你好','小明']) }}</p>
複製程式碼

const messages = {
  en: {
    message: {
      hello: '{0} world, I am {1}'
    }
  },
  zh: {
    message: {
      hello: '我是{1}, 世界,{0}!我是{1}!'
    }
  }
}

const i18n = new VueI18n({
  locale: 'zh',
  messages
})
複製程式碼

輸出:

<p>我是小明, 世界,你好!我是小明!</p>
複製程式碼

也支援傳入鍵值的形式,稱為“命名格式化”(Named formatting):

<p>{{ $t('message.goodbye', {day: "明天"}) }}</p>
複製程式碼

const messages = {
  zh: {
    message: {
      hello: '我是{1}, 世界,{0}!我是{1}!',
      goodbye: '再見!{day}見!'
    }
  }
}
複製程式碼

輸出:

<p>再見!明天見!</p>
複製程式碼

6.4 DateTime 本地化

<p>{{ $d(new Date(1945,7,15)) }}</p>
<p>{{ $d(new Date(1945,7,15), 'config2', 'ja-JP') }}</p>
複製程式碼

const dateTimeFormats = {
  'en-US': {
  },
  'ja-JP': {
    config1: {
      year: 'numeric', month: 'short', day: 'numeric'
    },
    config2: {
      year: 'numeric', month: 'short', day: 'numeric',
      weekday: 'short', hour: 'numeric', minute: 'numeric', hour12: true,
      era: 'short'
    }
  }
}

const messages = {
  en: {
  },
  ja: {
  }
}

const i18n = new VueI18n({
  locale: 'ja', // 區域設定
  messages, 
  dateTimeFormats // 多傳入一個日期時間格式
})
複製程式碼

輸出:

<p>1945/8/15</p>
<p>西暦1945年8月15日(水) 午前0:00</p>
複製程式碼

語法如程式碼所示,具體配置項可檢視 www.ecma-international.org/ecma-402/2.…

6.5 Number 本地化

<p>{{ $n(100, 'currency') }}</p>
<p>{{ $n(100, 'currency', 'ja-JP') }}</p>
複製程式碼

const numberFormats = {
   'en-US': {
     currency: {
       style: 'currency', currency: 'USD'
     }
   },
   'ja-JP': {
     currency: {
       style: 'currency', currency: 'JPY', currencyDisplay: 'symbol'
     }
   }
 }

const messages = {
  en: {
  },
  ja: {
  }
}

const i18n = new VueI18n({
  locale: 'ja', // 區域設定
  messages, 
  numberFormats // 多傳入一個數字格式
})
複製程式碼

輸出:

<p>$100.00</p>
<p>¥100</p>
複製程式碼

語法同樣不難理解,具體配置項可檢視 developer.mozilla.org/en-US/docs/…

6.6 基於元件的 i18n

除了在 Vue 的根例項中作為構造器引數傳入國際化設定外,也可以對單獨的 Vue 元件中設定 i18n,其作用域限於元件本身,和全域性國際化重複的欄位會優先顯示。

<div id="app">
  <p>{{ $t("message.hello") }}</p>
  <component1></component1>
</div>
複製程式碼

// Vue 根例項和全域性的`i18n`
const i18n = new VueI18n({
  locale: 'ja',
  messages: {
    en: {
      message: {
        hello: 'hello world',
        greeting: 'good morning'
      }
    },
    ja: {
      message: {
        hello: 'こんにちは、世界',
        greeting: 'おはようございます'
      }
    }
  }
})

// 定義元件
const Component1 = {
  template: `
    <div class="container">
     <p>Component1 locale messages: {{ $t("message.hello") }}</p>
     <p>Fallback global locale messages: {{ $t("message.greeting") }}</p>
   </div>`,
  i18n: { // 區域性的 `i18n` 
    messages: {
      en: { message: { hello: 'hello component1' } },
      ja: { message: { hello: 'こんにちは、component1' } }
    }
  }
}

new Vue({
  i18n,
  components: {
    Component1
  }
}).$mount('#app')
複製程式碼

輸出:

<div id="app">
  <p>こんにちは、世界</p>
  <div class="container">
    <p>Component1 locale messages: こんにちは、component1</p>
    <p>Fallback global locale messages: おはようございます</p>
  </div>
</div>
複製程式碼

6.7 元件插值

有時會有 操作失敗,請參考<a href="{1}">{0}</a>頁面總金額:<em>{0}</em>元 這類本地化資訊。

直觀的方法可能是將其作為幾段,避開 html 部分進行拼接;或是利用 v-html="$t('xxx')" 將其作為整體注入。

但以上兩種措施,要麼繁瑣而不優雅,要麼會帶來 XSS 風險。

在 vue-i18n 中,元件插值(Component interpolation)可以較好的解決此類問題:

<div id="app">
  
  <i18n path="info" tag="p">
    <span place="limit"> <!--注意 place 屬性的應用-->
      {{ changeLimit }}</span>
    <a place="action" 
       :href="changeUrl">
      {{ $t('change') }}</a>
  </i18n>
  
</div>
複製程式碼

const messages = {
  en: {
    info: 'You can {action} until {limit} minutes from departure.',
    change: 'change your flight'
  }
}

const i18n = new VueI18n({
  locale: 'en',
  messages, 
})

new Vue({
  i18n,
  data: {
    changeUrl: '/change',
    changeLimit: 15
  }
}).$mount('#app')
複製程式碼

輸出:

<div id="app">
    <p>
        You can 
        <a place="action" href="/change">change your flight</a> 
        until 
        <span place="limit">15</span> 
        minutes from departure.
    </p>
</div>
複製程式碼

6.8 i18next

另一個優秀的 Vue.js 國際化外掛是 i18next (github.com/i18next/i18…) ,本文不再展開介紹

VII. 新的 JS 國際化 API

在 2012 年末,ECMA International 推出了 Standard ECMA-402 標準的首個版本,也就是廣為人知的 ECMAScript Internationalization API。這個標準彌補了 ECMAScript 中早該支援的本地化方法。基本上所有現代瀏覽器都已經支援該 API。

7.1 Intl 全域性物件

Intl 物件是 ECMAScript 國際化 API 的一個名稱空間,它提供了精確的字串對比,數字格式化,日期和時間格式化。CollatorNumberFormatDateTimeFormat 物件的建構函式是 Intl 物件的屬性。

  • Intl.Collator collators的建構函式,用於啟用對語言敏感的字串比較的物件。
  • Intl.DateTimeFormat 用於啟用語言敏感的日期和時間格式的物件的建構函式。
  • Intl.NumberFormat 用於啟用語言敏感數字格式的物件的建構函式。
  • Intl.PluralRules 用於啟用多種敏感格式和多種語言語言規則的物件的建構函式。

正如我們在 2.3 中已經見過的 DateTimeFormat 例子,這幾種建構函式都使用同樣的模式來識別語言區域和確定使用哪一種語言格式:它們都接收 locales 和 options 引數。

options 引數必須是一個物件,其屬性值在不同的建構函式和方法中會有所變化。如果 options 引數未提供或者為 undefined,所有的屬性值則使用預設的。

再看幾個簡單的例子:

var number = 123456.789;

// 德語使用逗號作為小數點,使用.作為千位分隔符
console.log(new Intl.NumberFormat('de-DE').format(number));
// → 123.456,789

// 通過編號系統中的nu擴充套件鍵請求, 例如中文十進位制數字
console.log(new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(number));
// → 一二三,四五六.七八九


// 德語中, ä 使用 a 的排序
console.log(new Intl.Collator('de').compare('ä', 'z'));
// → -1

// 瑞典語中, ä 在 z 的後面
console.log(new Intl.Collator('sv').compare('ä', 'z'));
// → 1


var date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

// 使用24小時制
options = {
  year: 'numeric', month: 'numeric', day: 'numeric',
  hour: 'numeric', minute: 'numeric', second: 'numeric',
  hour12: false
};
console.log(date.toLocaleString('en-US', options));
// → "12/19/2012, 19:00:00"
複製程式碼

7.2 intl.js polyfill

對於一些沒有原生 Intl API 的老舊瀏覽器,可以使用 github.com/andyearnsha… 達到大部分功能的支援。

7.3 附:一個取得農曆日期的方法

在新的 API 出現之前,計算農曆是一件困難的事情,且有最大年份限制;採用新的 API 可以很好的解決這個問題。

以下方法改良自 jsfiddle.net/DerekL/mGXK…

function getLunarDate(date) {
  const TIAN_GAN = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
  const DI_ZHI = ["子", "醜", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
  const SHI = ["初", "十", "廿", "三"];
  const YUE = ["", "十"];
  const GE = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
  
  const locale = "zh-TW-u-ca-chinese";
  const fmt = (key, d=null)=>{
  	return Intl.DateTimeFormat(locale,{[key]:"numeric"}).format(d||date).match(/\d+/)[0];
  };
  const isLeapMonth = (d)=>{
  	let _date = new Date(date);
    _date.setDate(-d);
    return fmt("month", _date) === m;
  };
  
  let y = fmt("year");
  let m = fmt("month");
  let d = fmt("day");

  isL = isLeapMonth(d);

  y = TIAN_GAN[(y - 1) % 10]
  	+ DI_ZHI[(y - 1) % 12];
  m = (YUE[(m - 1) / 10 | 0]
  	+ GE[(m - 1) % 10]).replace(/^一$/, "正");
  d = (SHI[(d) / 10 | 0]
  	+ GE[(d - 1) % 10]).replace(/^十十$/, "初十").replace(/^廿十$/, "二十");

  return y + "年" + (isL ? "閏" : "") + m + "月" + d;
}

var date = new Date(1945,7,15);
var lunar = getLunarDate(date);
console.log(lunar); //乙酉年七月初八
複製程式碼

VIII. 參考資料

馬什麼梅?I什麼N?淺談 web 前端開發中的國際化

-- End --

相關文章