Mithril、Vue.jsの仮想DOM用のi18nライブラリ作った

いろいろ探してみたり、某node.js会長に聞いてみたりしたんですが、いまいち自分のニーズにあうi18nライブラリが見つからなかったので、一番理想に近い(けど一歩足りなかった)[roddeh-i18n](http://i18njs.com/)をベースに作ってみました。MITライセンスです。

https://i18n4v.js.org/

* 国際化機能をサポート
  * キーで単語を置き換える（元の単語もキーとして使用できます）
  * 複数形
  * フォーマッティング(変数に値を設定)
  * 文脈からの翻訳の選択（性別など）
* 小さくてポータブルなランタイム
  * ランタイムは移植性の高いES3で書かれ、gzipで1.8キロバイト
  * ランタイムは他に依存せず
  * common.jsおよびAMDとも利用可能。``<script>``タグを使用したロードも可能
* ブラウザで実行可能
  * 仮想DOM(Mithril, Vue.js)で使用
  * スタティックなHTMLの翻訳
* node.jsで実行可能
  * オフライン単位テストを簡単にする
  * サーバー側のレンダリングとかCLIツールとかでも
* 翻訳メンテナンス用のCLIツールを提供

メンテナンスツールはES6で書いているので実行にはnode.js 6.0以降じゃないとダメだと思いますが、ランタイムはES3なので、IE6みたいなブラウザや、Google Apps Scriptとかからも使えると思います。

そのうちReactとかRiotにも対応したいと思いつつ今はちょっと時間が取れてません。そのうち。

インストール
------------

```shell-session
$ npm install i18n4v 
```

既存のライブラリでは何が足りなかったのか？
------------------------------------

仮想DOMが広く使われるようになって、ウェブフロントエンドは完全にJavaScriptの世界になりました。ブラウザ専用みたいなのはたくさんありました。XMLHttpRequestで翻訳のテキストをサーバに取りに行くような・・・ですが

* ユニットテストはnode.jsでオフラインで実行することが多い(と思うし、node.jsでできたほうが速くて便利)、過度にブラウザに依存しているのは使いにくい
* サーバーサイドレンダリングをnode.jsで実行するなら、同じ辞書や同じ翻訳機能をnode.jsとブラウザで共有したい
* CLIツールのヘルプとかコメントも翻訳したい

ということで、node.jsでもブラウザでもユニバーサルな動作がするのが欲しいですよね。

ついでに、翻訳機能も、複数形対応は当然として、可能ならコンテキスト(主語が男性名詞か、女性名詞か)で訳文切り替えとかも欲しいです。このユニバーサル、複数形対応、コンテキスト対応の3つを満たすものはほとんどありませんでした。roddeh-i18nはこの点は問題なかったのですが、以下の機能も欲しかったので、これをベースに機能を追加しました

* 環境ごとのデフォルト言語の選択の補助機能の提供
* 仮想DOMとはいっても、画面すべてが仮想DOM配下におくわけではない。Mithrilでは動的変更される領域に限定して、小さくわけた複数の小パーツのみを仮想DOM化して、それ以外の部分を静的HTMLで、という運用ができる。その静的HTMLも翻訳したい。
* QtのLinguistとかgettextとかSphinxのi18n機能みたいに、辞書のメンテは自動化したい

基本の翻訳機能
---------------

辞書JSONを用意してadd関数で登録すると次のように翻訳ができます。

```js
const i18n = require('i18n4v');

i18n.translator.add({
    values:{
        "Hello": "こんにちは"
    }
});

i18n("Hello");  // -> こんにちは
```

複数形はキーの訳語を配列にして、その中に三項目の配列を入れます。それぞれ、数値の範囲(min, max)と、その時の訳語です。
nullを使うと、-∞、+∞の意味になります。``%n``で、指定された数値に置き換えることができます。``-%n``も使えます。ただし、細かい演算はできないので、イギリス英語では1階はground floor、2階がfirst floorというのはそれぞれ翻訳語を作らないと今は対応できません。

```js
i18n.translator.add({
    "values": {
        "%n comments": [
            [0, 0, "%n comments"],
            [1, 1, "%n comment"],
            [2, null, "%n comments"]
        ]
    }
});

i18n("%n comments", 0);    //  -> 0 comments
i18n("%n comments", 1);    //  -> 1 comment
i18n("%n comments", 2);    //  -> 2 comments
```

フォーマッティングは次のように書きます。

```js
i18n("Welcome %{name}", { name:"John" });    //  -> Welcome John
```

基本的に、キーに対する訳語が見つからないと、キーそのものが結果として使われますが、見つからなかった時のフォールバック文字列を指定することもできます。

```
i18n("_short_key", "This is a long piece of text")); // -> This is a long piece of text
```

2番目のパラメータが豊富ですが、同時に使うのであれば、

```js
i18n(キー, デフォルト文字列, 数値, フォーマットの引数)
```
の順番になります。

コンテキスト
---------------

男性名詞などの文脈に合わせた翻訳をするには、訳文をコンテキストごとに登録する必要があります。
matchesが条件の辞書ですが、ここでは性別だけですが、ここにいくつもの条件を追加することができます。

```js
i18n.translator.add({
  "contexts": [
    {
      "matches": {"gender": "male"},
      "values": {
        "%{name} updated their profile": "%{name} updated his profile"
      }
    },
    {
      "matches":{"gender": "female"},
      "values": {
        "%{name} updated their profile": "%{name} updated her profile"
      }
    }
  ]
});

i18n("%{name} updated their profile",
  { name: "John" },
  { gender: "male" }
); //  ->  John updated his profile

i18n("%{name} updated their profile",
  { name: "Jane" },
  { gender: "female" }
); //  ->  Jane updated her profile
```

静的HTMLの翻訳
-------------------

applyToHTML()メソッドを呼ぶと、タグに``data-i18n``属性が付いた静的HTMLをまとめて翻訳します。

```html
<article>
   <h1 data-i18n>Monty Python</h1>
</article>

<script>
i18n.translator.add({
    values: {
        "Monty Python": "モンティ・パイソン"
    }
});
i18n.translator.applyToHTML();
</script>
```

次の書き方をすると、翻訳のキーは「こんにちわ」ではなく、greetingになります。

```html
<span data-i18n="greeting">こんにちわ</span>
```

デフォルトではサニタイズしてからHTMLを更新するのですが、``data-i18n-safe``をつけると、翻訳文にタグが入っていてもそのまま挿入します。

```html
<span data-i18n data-i18n-safe>Hello</span>
```

翻訳言語の選択補助
----------------------

ブラウザの表示言語情報や、node.jsなら実行環境のLANG環境変数などを元に適切な言語を選択する機能も提供しています。node.jsの場合は[os-locale](https://www.npmjs.com/package/os-locale)パッケージを使って現在の言語設定を取得します。配列で渡した言語リストの中から、一番近い言語を返します。

```js
var languages = {
    en: require('./languages/en.json'),
    fr: require('./languages/fr.json'),
    de: require('./languages/de.json')
}

i18n.translator.selectLanguage(['en', 'de', 'fr'], function (err, lang) {
    i18n.translator.add(languages[lang] ? languages[lang] : languages.en);
});
```

ユーザの好みの言語を保存する機能もあります。ブラウザ限定ですが、次のコードを呼ぶと、localStorageに言語を保存します。一度保存すると、次からselectLanguageはその言語を優先して返します。

```js
i18n.translator.setLanguage('tlh');
```

Mithrilからの利用
-------------------

MithrilはJavaScriptでJSON形式で仮想DOMを作ります。もともとテンプレートフォーマットとかなくて、「JavaScriptでできることはすべてできる」というのがMithrilの仮想DOMの特徴です。このライブラリを使うのも、簡単です。

サニタイズはMithrilがやってくれるので、特に気にする必要はありませんが、逆にタグを入れたい時はMithrilのm.trustを使ってください。

```js
   view: function (ctrl) {
        return m('div', [
            m('h3', i18n('Language Select')),
            m('button', {onclick: ctrl.select.bind(ctrl, 'en')}, 'English'),
            m('button', {onclick: ctrl.select.bind(ctrl, 'ja')}, 'Japanese')
        ]);
    }
```

Mithrilのサンプルも[こちら](https://i18n4v.js.org/examples/mithril.html)にあります。

Vue.jsからの利用
------------------

Vue.js 2.0用のプラグインも作りました

```shell-session
$ npm install i18n4vue
```

Vue.jsのサンプルは[こちら](https://i18n4v.js.org/examples/vue.html)にあります。

```js
// プラグインを有効化
Vue.use(require('i18n4vue'));

// v-18nディレクティブで翻訳
<h3 v-i18n>Language Select</h3>

// .safeをつけるとサニタイズをオフにします
<h3 v-i18n.safe>Language Select</h3>

// i18nメソッド経由で使うこともできます。
<th v-for="label in dayOfWeekLabels">{{ i18n(label) }}</th>
```

つづき
-------

辞書メンテツールやサーバサイドの話は[Node.jsの仮想DOM用のi18nライブラリ作った](http://qiita.com/shibukawa/items/52dc43e7d66004593d85#_reference-377605840bea1110993b)を参照してください。