sundway / blog

oh~~
110 stars 6 forks source link

前端国际化 #9

Open sundway opened 7 years ago

sundway commented 7 years ago

何为国际化

国际化(Internationalization)通常在很多地方会用 i18n 代替,意思就是 I 加18个字母加 n。跟国际化还有一个类似的概念叫做本地化(Localization)通常用 L10n 表示。这是两个比较接近的概念,它之间有什么区别呢?W3C 的 Localization vs. Internationalization 这篇文档详细了介绍了这一点,对国际化理解还不是特别清晰的强烈建议读一下这篇文章。简单的理解就是,国际化就是为本地化做很多的前期工作,可以根据所做的事情是否属于只是为某一地区来区分国际化和本地化。国际化包含的东西很多,对于前端,最常接触到的就是将可本地化的元素与源代码或内容分开,以便可以根据需要根据用户的国际偏好来加载或选择本地化的替代。但国际化的工作远不止这些,例如国际化CDN 部署,Unicode 编码都可以算作国际化的范畴。

国际化 API

其实在 2012 年就已经拟定了国际化 API,详尽的内容可以参考 《ECMAScript Internationalization API Specification》。下面重点介绍几个 API 的使用:

Intl.Collator

Intl.Collator 是用于语言敏感字符串比较的 collators构造函数。语法:

new Intl.Collator([locales[, options]])
Intl.Collator.call(this[, locales[, options]])

上述语法中,locales 是可选参数,locales 的参数必须遵从 BCP 47 规范,locales 标记必须是 "en-US" 和 "zh-Hans-CN 等,这个标记包含了语言、地区和国家。完整的列表可以查看 IANA language subtag registry。options 也是可选参数,它包含了特定比较选项的对象。

示例:

var co1 = new Intl.Collator(["de-DE-u-co-phonebk"]);  
var co2 = new Intl.Collator(["de-DE"]);  
var co3 = new Intl.Collator(["en-US"]);  

var arr = ["ä", "ad", "af", "a"];  

if (console && console.log) {  
    console.log(arr.sort(co1.compare));  // Returns a,ad,ä,af  
    console.log(arr.sort(co2.compare));  // Returns a,ä,ad,af  
    console.log(arr.sort(co3.compare));  // Returns a,ä,ad,af  
}  

Intl.DateTimeFormat

Intl.DateTimeFormat是根据语言来格式化日期和时间的类的构造器类。

new Intl.DateTimeFormat([locales[, options]])
Intl.DateTimeFormat.call(this[, locales[, options]])

Intl.DateTimeFormat是根据语言来格式化日期和时间的类的构造器类。可以使用 options 参数来自定义 日期时间格式化方法返回的字符串。

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

// 下面是假定的所在时区
// 洛杉矶(America/Los_Angeles for the US)

// 美式英语(US English) 使用  month-day-year 格式
console.log(new Intl.DateTimeFormat('en-US').format(date));
// → "12/19/2012"

// 英式英语(British English) 使用 day-month-year 格式
console.log(new Intl.DateTimeFormat('en-GB').format(date));
// → "20/12/2012"

// 韩国使用 year-month-day 格式
console.log(new Intl.DateTimeFormat('ko-KR').format(date));
// → "2012. 12. 20."

//大部分阿拉伯国家使用阿拉伯字母(real Arabic digits)
console.log(new Intl.DateTimeFormat('ar-EG').format(date));
// → "٢٠‏/١٢‏/٢٠١٢"

//在日本,应用可能想要使用日本日历,
//2012 是平成24年(平成是是日本天皇明仁的年号,由1989年1月8日起开始计算直至现在)
console.log(new Intl.DateTimeFormat('ja-JP-u-ca-japanese').format(date));
// → "平成24/12/20"

//当请求一个语言可能不支持,如巴厘(ban),若有备用的语言印尼语(id),
//那么将使用印尼语(id)
console.log(new Intl.DateTimeFormat(['ban', 'id']).format(date));
// → "20/12/2012"

Intl.NumberFormat

Intl.NumberFormat是对语言敏感的格式化数字类的构造器类。

new Intl.NumberFormat([locales[, options]])
Intl.NumberFormat.call(this[, locales[, options]])

示例:

var number = 123456.789;

// 德语使用逗号作为小数点,使用.作为千位分隔符
console.log(new Intl.NumberFormat('de-DE').format(number));
// → 123.456,789

// 大多数阿拉伯语国家使用阿拉伯语数字
console.log(new Intl.NumberFormat('ar-EG').format(number));
// → ١٢٣٤٥٦٫٧٨٩

// India uses thousands/lakh/crore separators
console.log(new Intl.NumberFormat('en-IN').format(number));
// → 1,23,456.789

// 通过编号系统中的nu扩展键请求, 例如中文十进制数字
console.log(new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(number));
// → 一二三,四五六.七八九

//当请求的语言不被支持,例如巴里,包含一个回滚语言印尼,这时候就会使用印尼语
console.log(new Intl.NumberFormat(['ban', 'id']).format(number));
// → 123.456,789

这个例子显示了一些本地化的数字格式的一些变化。为了获得用于您的应用程序的用户界面的语言格式,请确保设定了语言(可能还有一些回退语言)参数。

以下各浏览器的支持情况:

可以看到个浏览器对 Intl API 的支持已经相当不错了。对于一些不兼容的浏览器,我们可以引入 Intl polyfill

生产环境中的应用

在生产环境中我们一般会引用第三方库,不同的技术方案引入的库也会不同。以下是几个主流的库/框架的解决方案:

下面以 react 为例,这里建议直接可以使用阿里开源的 react-intl-universal,相比于 Yahoo react-intl API,它不仅支持 react 组件,同时也支持原生 js。

import intl from 'react-intl-universal';

// locale data
const locales = {
  "en-US": require('./locales/en-US.js'),
  "zh-CN": require('./locales/zh-CN.js'),
};

class App extends Component {

  state = {initDone: false}

  componentDidMount() {
    this.loadLocales();
  }

  loadLocales() {
    // init method will load CLDR locale data according to currentLocale
    // react-intl-universal is singleton, so you should init it only once in your app
    intl.init({
      currentLocale: 'en-US', // TODO: determine locale here
      locales,
    })
    .then(() => {
      // After loading CLDR locale data, start to render
      this.setState({initDone: true});
    });
  }

  render() {
    return (
      this.state.initDone &&
      <div>
        {intl.get('SIMPLE')}
      </div>
    );
  }

}

当然有些项目没有使用上面任何一个库/框架,例如阿里小蜜。也有相应地方案去解决。主要思路:

阿里小蜜中使用的是 nunjucks 模版引擎,那么这里可以使用 nunjucks-intl。如果是其他模版工具,也可以自己在 polyfill 上写一个简单的实现。

最后,推荐一下 Yahoo 的 FormatJS,官方给出的解释是一系列 JavaScript 库的集合,主要用来格式化数字,日期和字符串。它包含一系列核心代码,而这些代码建立在 JS 原生 Intl 以及各种 i18n 标准之上的。

这篇文章主要简单的介绍了国际化以及国际的解决方案,但是国际化中还存在很多挑战,如国际化样式适配、map 表维护等。这些将在下一篇《国际化项目应用》中讲解。

ryerh commented 7 years ago

以下为个人见解:

凡是诸如 react-intl-universal 这类用 key: value 定义个字典的方案,实际项目大了之后用起来挺难受的,因为:

  1. 要考虑字典怎么设计(尽量重用 key)
  2. 组件里调用的时候 {intl.get(SOME_KEY)},内容就已经不可读了,需要看一下字典才知道对应什么含义

有个比较小众的多语言方案 — i18n-calypsoi18n-calypso 基于广泛使用的 gettext 方式,通过静态分析查找源码文件中的多语言调用函数,提取词条创建 po 文件,配合 webpack po-loader 即可加载到项目中,使用起来更省力。

kenberkeley commented 7 years ago

@ryerh 还可以更进一步:https://github.com/onewaytech/i18n-static

sundway commented 7 years ago

@ryerh @kenberkeley 感谢分享,我会有后续的分享,关于如何解决工程项目中的问题。

AceLeeWinnie commented 6 years ago

谢谢 i18n-pick 这个好的工具,但是无法解析 new Error('这是个错误') 这样的类型,原因和 babel 有关,是否可以添加 babel 扩展包把这些 babel-type 也囊括在内

YaoKaiLun commented 5 years ago

@sundway 后续呢...

Myhappyboy commented 5 years ago

@ryerh 项目在打包后在edge浏览器中报错“未指明错误” import Vue from 'vue' import locale from 'element-ui/lib/locale'; import VueI18n from 'vue-i18n' import messages from './langes' Vue.use(VueI18n) const i18n = new VueI18n({ locale: localStorage.langauage || 'en', messages, }) locale.i18n((key, value) => i18n.t(key, value))

export default i18n