實戰React App的i18n

記得我剛來咱們公司的時候,接手如今負責的項目的時候,我就發覺了一個問題:全部的文本資源都是硬編碼在代碼裏面。這固然會帶來不少問題。但考慮到我負責的這個項目是公司內部的管理工具,同時大部分用戶都是中國人,所以抽離文本資源,作i18n的需求並非十分強烈。javascript

這周公司招了一位外籍員工。我並不肯定她是哪一國人,不過從口音上來判斷,以及言談間她曾經提到的加利福尼亞州,我想應該是一位美國女性。老大說她會和其餘的PM同樣,居住在廈門,遠程工做,偶爾來辦公室上班。而且她也會使用我負責的這個工具。html

如今i18n就有比較強烈的需求了。有必要出一個合理的架構,一勞永逸的解決問題。java

i18n的主要關注點

i18n是Internationalization的縮寫,實際上i18n應該是指建立或者調整產品,使得產品具備能輕鬆適配指定的語言和文化的能力。固然,咱們還有另一個概念,叫作Localization(簡寫L10n),也就是本地化。L10n正確的說是指已經全球化的產品,適配某一個具體語言和文化的這一個過程。node

有點繞口,簡單說就是,i18n就是給產品添加新特性,使產品可以支持對多種語言和文化(貨幣,時間等等)。而L10n就是產品具體實現某一種語言和文化的過程。react

回過頭來,i18n有這麼幾個主要的關注點:webpack

  1. Date and times formattinggit

  2. Number formattingweb

  3. Language sensitive string comparison架構

  4. Pluralizationapp

Date and times formatting

不一樣國家對應的日期格式其實都是不一樣的,儘管我不以爲十分複雜,不過細節的處理上也是有不少選擇:

  1. weekday,你能夠設置成顯示全名字,好比zh-CN的星期四,en-US的Thursday等等

  2. month,你能夠設置成數字形式,全名,短名,相似於12,December,Dec

...

完整的例子:

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

// Results below use the time zone of America/Los_Angeles (UTC-0800, Pacific Standard Time)

// US English uses month-day-year order
console.log(new Intl.DateTimeFormat('en-US').format(date));
// → "12/19/2012"

// British English uses day-month-year order
console.log(new Intl.DateTimeFormat('en-GB').format(date));
// → "19/12/2012"

// Korean uses year-month-day order
console.log(new Intl.DateTimeFormat('ko-KR').format(date));
// → "2012. 12. 19."

// Arabic in most Arabic speaking countries uses real Arabic digits
console.log(new Intl.DateTimeFormat('ar-EG').format(date));
// → "١٩‏/١٢‏/٢٠١٢"

// for Japanese, applications may want to use the Japanese calendar,
// where 2012 was the year 24 of the Heisei era
console.log(new Intl.DateTimeFormat('ja-JP-u-ca-japanese').format(date));
// → "24/12/19"

// when requesting a language that may not be supported, such as
// Balinese, include a fallback language, in this case Indonesian
console.log(new Intl.DateTimeFormat(['ban', 'id']).format(date));
// → "19/12/2012"

var date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));
// request a weekday along with a long date
var options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
// an application may want to use UTC and make that visible
options.timeZone = 'UTC';
options.timeZoneName = 'short';
console.log(new Intl.DateTimeFormat('en-US', options).format(date));
// → "Thursday, December 20, 2012, GMT"

Number formatting以及Pluralization

數字的格式化,這個比較有趣。這裏說的數字,包含了貨幣,百分比,浮點數。其中貨幣的顯示應該是相對比較複雜的。就以en-US來講,1000美圓一般顯示成$1,000.00,而1000人民幣則會顯示成¥1,000.00。貨幣的符號,以及數字分割方式各個國家都存在不一樣。

var number = 123456.789;

// German uses comma as decimal separator and period for thousands
console.log(new Intl.NumberFormat('de-DE').format(number));
// → 123.456,789

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

// the nu extension key requests a numbering system, e.g. Chinese decimal
console.log(new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(number));
// → 一二三,四五六.七八九

var number = 123456.789;

// request a currency format
console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(number));
// → 123.456,79 €

// the Japanese yen doesn't use a minor unit
console.log(new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(number));
// → ¥123,457

涉及到數字的,還有另一個問題,那就是語言的複數形式。中文彷佛是沒有複數形式的,好比咱們常常說,一隻兔子,兩隻兔子。可是若是你用英語,你就能明顯發覺不對。在英語裏,應該說one rabbit,two rabbits,many rabbits。是的,英語裏主要有兩種複數形式。

那麼有沒有其餘的 複數形式?回答固然是確定的,好比波蘭語。在波蘭語,兔子一詞是królik,它的複數形式有這麼幾種狀況:

  1. 兔子的數量是1,那麼應該這麼說,królik

  2. 若是兔子的數量在2-4之間,那麼應該說,królika

  3. 若是兔子的數量不是1,而且數量在0 - 1之間,或者5 - 9之間,或者12 - 14之間,都用królików

  4. 其餘狀況統一用króliki

解決方案的設計

項目背景

  1. 使用facebook官方的create-react-app腳手架建立的react app

關注點

目前個人解決方案有這麼幾個關注點:

  1. 文本資源要可以輕易的導出

  2. 文本資源要孤立,避免和程序實現的耦合

  3. 提供極簡的接口方法設計

  4. 處理語言複數形式的庫,應該要能很好的拓展

技術選型

  1. FormatJS, a modular collection of JavaScript libraries for internationalization that are focused on formatting numbers, dates, and strings for displaying to people.

解決方案

項目的目錄結構

/--
  |--node_modules
  |--public
  |--src
    |--app
    |--common
    |--components
    |--configs
    |--i18n
      |--app
        |--routes
          |--setting
            |--en-US.js
            |--zh-CN.js
        |--index_en-US.js
      |--common
        |--index_en-US.js
      |--components
        |--index_en-US.js
      |--index_en-US.js
      |--index_zh-CN.js
    |--index.js
    |--logo.svg
    |--setupTests.js

如上文所示,我將全部的文本資源都獨立出來,單獨存放在了i18n這個文件夾下。實際上,這些文本資源是有本身獨立的命名空間的,好比/src/app相關的文本資源,就會單獨放在這個文件夾下。其餘的好比/src/common//src/components/就以此類推。

Intl類

這個類很簡單,封裝了處理文本資源的相關方法。getText的參數key須要特別注意,這個參數應該是絕對路徑,好比app.routes.setting.preferences這樣。那麼,相關的資源應該是要放在/src/i18n/app/routes/setting/en-US.js文件裏。

class Intl {
  static COMP_COMMON_TEXT = 'components.Common';
  constructor(locale, resource) {
    this.locale = locale || 'zh-CN';
    this.resource = resource;
  }
  getCommonText(key, params = {}) {
    return this.getText(`${Intl.COMP_COMMON_TEXT}.${key}`, params);
  }
  getText(key, params = {}) {
    let textResource = '';
    let source = this.resource;
    const locale = this.locale;
    const properties = key.split('.');
    const hasOwnProperty = Object.prototype.hasOwnProperty;
    properties.forEach((property, index) => {
      const stillNameSpace = index !== properties.length - 1;
      if (stillNameSpace) {
        source = source[property];
      } else if (hasOwnProperty.call(source[property], 'default')) {
        textResource = source[property].default;
      } else {
        textResource = source[property] || '';
      }
    });
    const msg = new IntlMessageFormat(textResource, locale);
    return msg.format(params);
  }
}

IntlProvider

這是一個React組件。這裏咱們要利用React提供的Context這一特性,讓整個React App範圍內,都會從上下文中獲得getText的方法。

咱們都知道,Web app初始化的時候加載的Javascript腳本是越小越好,而且咱們應該盡力保證按需加載所須要的資源。這也是咱們爲何利用WebPack提供的Code Splitting機制讓WebPack在打包的時候,切分出單獨的chunk,減小包的體積。

在WebPack 1.x的時候,咱們可使用require.ensure()。但這個是WebPack本身的語法,並不是標準,同時這個語法還會破壞Jest的測試,並非一個很好的選擇。WebPack 2.x之後就開始提供基於import()的Code Splitting機制。所以咱們應該利用起來。

具體的兩個文檔:

  1. WebPack的Code Splitting with ES2015

  2. Dynamic import() proposal

class IntlProvider extends React.Component {
  static DEFAULT_LOCALE = 'zh-CN';
  static propTypes = {
    locale: PropTypes.string,
    children: PropTypes.element,
  };
  static defaultProps = {
    locale: 'zh-CN',
    children: null,
  };
  static childContextTypes = {
    getText: PropTypes.func,
    getCommonText: PropTypes.func,
  };
  state = {};
  constructor(props, context) {
    super(props, context);
    this.childContext = new Intl(props.locale);
  }
  async componentWillMount() {
    const { locale } = this.props;
    const lang = await import(`../i18n/index_${locale}.js`);
    this.childContext = new Intl(locale, lang);
    this.setState({
      lang,
    });
  }
  getChildContext() {
    if (!this.childContext) {
      return {
        getText: (key, params) => '',
        getCommonText: (key, params) => '',
      };
    }
    return {
      getText: (key, params) => this.childContext.getText(key, params),
      getCommonText: (key, params) => this.childContext.getCommonText(key, params),
    };
  }
  render() {
    const comp = (!this.state.lang)
    ? null
    : React.Children.only(this.props.children);
    return comp;
  }
}

App

使用的時候也是至關簡單,很少說,直接上代碼。

class App extends React.PureComponent {
  render() {
    const { preferences } = this.props;
    return (
      <IntlProvider locale={preferences.language}>
        <div>{this.props.children}</div>
      </IntlProvider>
    );
  }
}

參考文檔

相關文章
相關標籤/搜索