马什么梅?I什么N?浅谈 web 前端开发中的国际化

5,697 阅读12分钟

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 的页面。

#为何要学好普通话#: 天不怕,地不怕,就怕南方人说儿化音

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. 参考资料

-- End --