発明のための再発明

内部動作のような、プログラムを書くときの参考になることを書きます

i18nの書き方 from JavaScript

Webサイトを多言語化する

最近多言語化に関する仕事をしている関係でi18n(l10n)用のライブラリを色々と見たので、 i18nライブラリによくある使い方を紹介します。 とは言ってもほとんどのライブラリは同じような書き方をするのでJavaScriptを例にします。

ライブラリ一覧

書き方の紹介として使用するものはawasome-javascriptにあるものと、他に気になったものを選びました。

ただし、この記事の目的は「書き方」を紹介するもので「ライブラリ」を紹介するものではありません。 動くことは確認しましたがどのくらいまともに動くかはチェックしていないのでご注意ください。

  1. Polyglot
    Airbnbが公開するシンプルなライブラリ
  2. i18next
    色々な環境やフレームワークで動くことを売りとしたライブラリ。
  3. BabelFish
    Polyglotよりは色々出来そうなライブラリ。PerlRuby用にも書かれている。
  4. INTL
    ECMAScriptの標準API。文字列比較とフォーマッターに使える。
  5. Globalize
    CLDR準拠のフォーマットを作成することを目標としたフォーマット用ライブラリ。

翻訳文章を取得する

gettext

基本的にi18ngettextの方法が使われます。 つまり、

  1. キーワードと対応する文章を登録する
  2. キーワードと言語を基に文章を取得する

の2段階で多言語化します。この機能はほぼ全てのi18nライブラリが提供する基本的な機能です。

Polyglotを参考にすると、下のようになります。

const polyglot = new Polyglot({locale: 'en'})
polyglot.extend({
  hello: "Hello world",
  welcome: {
    capital: "WELCOME!"
  }
})

polyglot.t("hello")
// -> "Hello world"

polyglot.t("welcome.capital")
// -> "WELCOME!"

中にはフォールバックや複数のキーワードを使えるようにするものもあります。 i18nextでは対応するキーワードがない場合のフォールバックを次のように行います。

i18next.init({
  lng: 'en',
  fallbackLng: 'fr',
  resources: {
    'en': {
      'translation': {
        'key': '"key" exists',
      }
    },
    'fr': {
      'translation': {
        'key': 'clé',
        'fallback': 'se retirer'
      }
    }
  }
}, () => {
  // 最初のキーない場合は次のキーを参照する
  i18next.t(['no-key', 'key'])
  // -> '"key" exists'

  i18next.t('fallback')
  // -> "se retirer"
})

変数を使用する

文章の中の特定の文章を変えたいたい時には、変数が使えると便利ですね。 変数を表す書き方はライブラリで個性がありますが、やることは共通していて、文字列を出力するだけです。

BabelFishでは #{変数名} と書くことで変数を代入します。

const fish = new BabelFish('en-GB')
fish.addPhrase('en-GB', 'hello', 'Hello, #{user.name}.')
fish.t('en-GB', 'demo.hello', {user: {name: 'Summy'}})
// -> "Hello, Summy"

printf

変数を直接使用するのではなく、書式文字列を使用する方法もあります。 少し面倒ですがi18nextでは以下のように書けます。

i18next.init({
  lng: 'en',
  resources: {
    'en': {
      'translation': {
        'sprintf': "Hello %s"
      }
    }
  }
}, () => {
  i18next.t('sprintf', {postProcess: 'sprintf', sprintf: ['Bob']})
  // -> "Hello Bob"
})

複数形に対応する

複数形対応は、i18nライブラリの花型です。 日本語は複数形を意識しないので「単複の差で変わるだけでしょ」なんて考えてしまいますが、実際にはとても複雑怪奇です。 Mozillaローカライゼイションと複数形のページには14種類の規則が書かれています。 例えば、英語では「1」と「それ以外」を区別して変化しますが、フランス語では「0か1」が同じ変化になり、ロシア語では「下一桁が1だが11で終わらない」時に同じ変化をするようです。 このような複雑性をどのようにサポートするか(切り捨てるか)でライブラリの複数形対応に差が出てきます。 (例ではロシア語を使用していますが、ロシア語がわからないので変化していることが確認出来るようにだけしました。すいません。)

Polyglotでは翻訳文の中に区切りを入れてそれぞれの複数形を書くことで対応します。

const polyglot = new Polyglot({locale: 'ru'})
polyglot.extend({
  cars: "%{smart_count} car1 |||| %{smart_count} car2 |||| %{smart_count} cars3",
})

polyglot.t('num_cars', 1)
// -> "1 car1"

polyglot.t('num_cars', 2)
// -> "2 car2"

polyglot.t('num_cars', 11)
// -> "11 car3"

polyglot.t('num_cars', 21)
// -> "21 car1"

i18nextではキーワードの後ろに変化の番号をつけて、それぞれの複数形の翻訳文を書きます。

i18next.init({
  lng: 'ru',
  resources: {
    'ru': {
      'translation': {
        'plural_car_0': "{{count}} car1",
        'plural_car_1': "{{count}} car2",
        'plural_car_2': "{{count}} car3"
      }
    }
  }
}, () => {
  i18next.t('plural_car', {count: 1})
  // -> "1 car1"

  i18next.t('plural_car', {count: 2})
  // -> "2 car2"

  i18next.t('plural_car', {count: 11})
  // -> "11 car3"

  i18next.t('plural_car', {count: 21})
  // -> "21 car1"
})

BabelFishではPolyglotと同じように翻訳文の中に複数形を書きます。

const fish = new BabelFish('ru-RU')
fish.addPhrase('ru-RU', 'plural', '#{count} ((car1|car2|car3))');
fish.t('ru-RU', 'plural', 1)
// -> "1 car1"

fish.t('ru-RU', 'plural', 2)
// -> "2 car2"

fish.t('ru-RU', 'plural', 11)
// -> "11 car3"

fish.t('ru-RU', 'plural', 21)
// -> "21 car1"

Formatter

多言語化に必要な機能は翻訳文の取得だけではありません。 数値や日付は言語によって句切れや順番が違うので、注意する必要があります。 そんな時に参考になるのがCLDRです。 ここには数値や日付だけでなく、都市や単位の表記など多くの情報がありとても便利です。 JSONになったデータはGitHubにまとめられています。 ここで紹介するIntlとGlobalizeは両方ともCLDRに準拠しようとしています。

数値

国によってカンマの位置や数字の表記に差が出ます。 例えば、インドでは2桁区切りで、中国では漢数字を使います。

Intlを使うと、下のようになります。

new Intl.NumberFormat('en-IN').format(123456789)
// -> "12,34,56,789"

new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(123456789)
//-> "一二三,四五六,七八九"

更にIntlにはtoLocaleString()も用意されています。

const number = 123456789
number.toLocaleString()
// -> "123,456,789"

通貨

通貨は数値の問題に加えて、通貨記号をどう表記するかや、前後どちらに書くかという問題があります。 例えば、フランスでは通貨記号を後ろに書き、日本では中国元を「元」としますが、中国では「¥」と表記します。

この違いをIntlは言語と通貨を併せることによって解決しています。

new Intl.NumberFormat("fr", {style: 'currency', currency: 'EUR'}).format(123456.789)
// -> "123 456,79 €"

new Intl.NumberFormat("ja", {style: 'currency', currency: 'EUR'}).format(123456.789)
// -> "€123,456.79"

new Intl.NumberFormat("ja", {style: 'currency', currency: 'JPY'}).format(123456.789)
// -> "¥123,457"

new Intl.NumberFormat("ja", {style: 'currency', currency: 'CNY'}).format(123456.789)
// -> "元123,456.79"

new Intl.NumberFormat("zh", {style: 'currency', currency: 'CNY'}).format(123456.789)
// -> "¥123,456.79"

日付

日本では日付をyy/MM/ddの順番で書きますが、アメリカではMM/dd/yyの順で書きます。 これにも対応する必要があります。

Globalizeでは下のように書きます。

Globalize("en").formatDate(new Date(2016, 2, 9))
// -> "3/9/2016"

Globalize("ja").formatDate(new Date(2016, 2, 9))
// -> "2016/3/9"

Intlでは元号も出せます。

Intl.DateTimeFormat('ja-JP-u-ca-japanese').format(new Date(2016, 2, 9))
// -> "平成28/3/9"

他に多言語で表示するために必要なこと

これまで、i18nライブラリを参考に多言語化に使用される書き方を紹介しました。 しかし、これらのライブラリは翻訳機能は提供しません。そのため、ほかにも考えなければならないことがあります。

誰が翻訳するのか

内部の人や翻訳会社、機械翻訳などで対応することになります。 外部に依頼する場合には時間がかかるので、余裕を持つ必要があります。

翻訳の更新

翻訳に限らず、ページ内容は常に変更されるため、文章が変更された時に翻訳も変更する体制が必要です。 翻訳文を常に充てる必要があるのであれば公開前に翻訳する時間を用意する必要がありますが、 翻訳文を急がないのであれば先に元言語を変更し、文章公開後に余裕を持って翻訳を反映することも出来ます。 また、奥の手として機械翻訳をして公開し、後から人間が翻訳するという手もあります。

翻訳データの管理

バージョン管理をgitでするか、DBを使用するかで差がでます。 ファイル管理するなら、JavaScriptならJSONを、他の言語ではYAMLやTOMLなど各言語でデファクトとなっているフォーマットで保存すれば十分でしょう。