Gaia ローカライズするコードのベストプラクティス

この記事ではローカライズ可能なGaiaアプリを書くためのベストプラクティスを集めています。Gaiaプロジェクトで開発している場合、これらをできるだけ守るべきです。これらは自分でFirefox OS のアプリを作っている人にも役立ちます。

: 下記に記載した機能を使う上の基礎を読まないといけない場合、Firefox OS アプリをローカライズする が良いリファレンスガイドになるでしょう。

: もう一つ、時間を取って読むべきドキュメントはこれです: Localization content best practices。これはより汎用的で ( Firefox OS特有でなく)、コンテンツ文字列を出来る限りローカライズ可能にするためのベストプラクティスを網羅しています。一方、下記はコードに文字列を実装することについてです。

UIのローカライズ

ローカライズ可能なコードを書く最良の方法は、l10n【訳注: localization、つまりローカライゼーションの省略系】の論理を可能な限り宣言的HTMLに移動する事です。

常にHTML 要素を data-l10n-iddata-l10n-args とマークアップして、必要な時のみだけに JavaScript を使って、set/remove/update します。もはやオリジナルコンテンツをHTMLに留める必要はありません。

宣言型APIを使う

下記は JavaScript 駆動のローカライズによって上手にローカライズされた例です。 コードはきわどいものでなく、ガードが不要で、あらゆるロケール上で言語の変更に対し適切に反応します。HTMLは英語コンテンツすら持っていないのに注意して下さい。L10n.js は自身の縮退機能を使って、HTMLソースに記述されたあらゆる内容が実行時に常に置換されます。

<h1 data-l10n-id="appName" />
<h2 data-l10n-id="summary" />
<article>
  <p id="author" />
  <button id="actionButton" />
</article>
actionButtonElem.setAttribute('data-l10n-id', newArticle ? 'saveBtnLabel' : 'updateBtnLabel');

navigator.mozL10n.setAttributes(authorElem, 'articleAuthor', {
  'name': 'John Smith'
});
appName = My App
saveBtnLabel = Save
updateBtnLabel = Update
articleAuthor = The author of this article is {{ name }}

Untranslation

ノードが翻訳/再翻訳される事を停止する必要がある場合、要素から l10nId を除去する必要があります。

document.getElementById('node').removeAttribute('data-l10n-id');

このアプローチの唯一の制限は、ノードをきれいにする良い方法がない事で、ゆえにユーザが手動で値を翻訳したり、ローカライズされた属性を除去する事に依存しています:

document.getElementById('node').textContent = null;
document.getElementById('node').removeAttribute('placeholder');

将来に我々は、l10nIdがセットされない時に、値の翻訳や属性を自動的に除去されるようになるのを望んでています。

子要素を持つ要素には l10n-id をセットしない

大事な考慮事項は、要素に l10n-id をセットした時、L10n.js は要素のレンダリングを引き受けて、ゆえにあらゆる子ノードはローカライズされた文字列で上書きされる事です。将来的にはShadow DOM同様の何かを得て、要素のローカライズ版をレンダリングさえするかもしれません。

DOM断片をローカライズするには、下記を見て下さい。

mozL10n.get を使用しない

古いl10nパラダイムからよく使われているアンチパターンの一つに、固まりからl10n文字列を取得する同期メソッドがあります。mozL10n.get は同期的で、mozL10n.once や mozL10n.ready からコードを保護するのが求められるため、我々はそれを避けるのを推奨しています。つまりこれは再翻訳の動作をしないことにもなります。将来的には次の節で述べる事例が解決したら、このメソッド全体を削除する計画です。

こう書く代わりに:

// 悪い例

var elem1 = document.createElement('p');
elem1.textContent = navigator.mozL10n.get('helloMsg');

var elem2 = document.createElement('input');
elem2.placeholder = navigator.mozL10n.get('msgPlaceholder');

var elem3 = document.createElement('button');
elem3.ariaLabel = navigator.mozL10n.get('volumeLabel');

// .properties
helloMsg = Hello World
msgPlaceholder = Enter password
volumeLabel = Switch volume

これを使います:

// 良い例

var elem1 = document.createElement('p');
elem1.setAttribute('data-l10n-id', 'helloMsg');

var elem2 = document.createElement('input');
elem2.setAttribute('data-l10n-id', 'passwordInput');

var elem3 = document.createElement('button');
elem3.setAttribute('data-l10n-id', 'volumeButton');

// .properties
helloMsg = Hello World
passwordInput.placeholder = Enter password
volumeButton.ariaLabel = Switch volume

setAttribute も mozL10n.setAttributes も使用できないまれな場合には、非レースな非同期メソッドの  L10n.formatValue() を使用できます。

mozL10n.get の使用が正当化される時の例外

現状、mozL10n.get が必要になる3つの場合があります:

例外1: アプリ外に文字列を渡す

コードで外部のAPI — 例えば alert(), confirm(), や navigator.mozMobileMessage — に文字列を送る必要がある時、文字列の解決は出来るだけ遅らせるべきです。なのでこう書く代わりに:

// 悪い例
function sendText(msg) {
  navigator.mozMobileMessage.send(number, msg);
}

sendText(navigator.mozL10n.get('confirmationMessage'));

こう書きます:

// 良い例
function sendText(l10nId) {
  var msg = navigator.mozL10n.get(l10nId);
  navigator.mozMobileMessage.send(number, msg);
}

sendText('confirmationMessage');

API の動作がもっと保証され、デバッグが容易で拡張可能に (この記事の Writing APIs that operate on L10nIDs 節を見て下さい) なります。

例外2: DOMフラグメントをローカライズする

宣言型プログラミングがまだカバーしていない場所に、DOMフラグメントのローカライズがあります。こんなコードを想像してみて下さい:

<p>See our <a href="http://www.mozilla.org">website</a> for more information!</p>

このコードをローカライズ可能にする最良の方法は、このように l10n 文字列を作る事です:

.properties:
seeLink = See our <a href="{{ url }}">website</a> for more information!

html:
<p data-l10n-id="seeLink" data-l10n-args='{"url": "http://www.mozilla.org"}'></p>

フラグメント内にこれ以上のマークアップがない場合に動作するでしょう。下記の例を考えて下さい:

<p>See our <a href="http://www.mozilla.org" class="external big" id="foo">website</a> for more information!</p>

この例では、classid といった属性を l10n 文字列に付けるのは最適ではありません。そのマークアップへのあらゆる変更をすると、全ての翻訳の文字列変更が必要になるからです。文字列をいくつかの塊に分けて JavaScript で連結するのは、ローカライゼーションでDOM フラグメント内の要素を並び替えることがありえるため、誤りです。

将来的には、L10n.js が DOM オーバーレイ と呼ばれる補間戦略を提供して、開発者の提供した DOM フラグメントに翻訳をマージできるようになるでしょう。これが使えるようになるまでは、代替として mozL10n.get を使わなければなりません。

seeLink = See our {{ link }} for more information!
linkText = website
<p data-l10n-id="seeLink"></p>
navigator.mozL10n.setAttributes(elem, 'seeLink', {
  'link': '<a href="http://www.mozilla.org" class="external big" id="foo">' + navigator.mozL10n.get('linkText') + '</a>'
});

このコードは mozL10n.get を使うため、l10n リソースがロードされた後に実行されるのが保証される必要があります、なのでコード上で mozL10n.once のラッパーを使用します。

例外3: 日付/時刻をフォーマットする

日付/時刻をフォーマットするには、現在このようなパターンを使います:

var currentDate = new Date();
var f = new navigator.mozL10n.DateTimeFormat();
var format = navigator.mozL10n.get('shortTimeFormat');
elem.textContent = f.localeFormat(currentDate, format);

もう一度、mozL10n.get を使うために、mozL10n.once を使ってコードを保護して、レースコンディションを防ぎます。

将来的には日付/時刻フォーマットをマクロにする計画です。これでローカライズ担当が文字列内の日付フォーマット方法を決めることができて、このケースは削除されます。

ユーザー提供文字列や l10nIDsで動作するコードの書き方

この例では、ローカライズ可能でない歌のタイトルや、ローカライゼーションリソースを由来とする"Unknown Track"という文字列があります。

これには2パターンのアプローチがあります:

パターン1:

<h1 id="titleElement />
function updateScreen(track) {
  var titleElement = document.getElementById('titleElement');
  if (track.title) {
    navigator.mozL10n.setAttributes(titleElement, 'trackTitle', {
      'title': track.title
    });
  } else {
    navigator.mozL10n.setAttributes(titleElement, 'trackTitleUnknown');
  }
}
trackTitle = {{ title }}
trackTitleUnknown = Unknown Track

パターン2:

<h1 id="titleElement />
function updateScreen(track) {
  var titleElement = document.getElementById('titleElement');
  if (track.title) {
    titleElement.removeAttribute('data-l10n-id');
    titleElement.textContent = track.title;
  } else {
    titleElement.setAttribute('data-l10n-id', 'trackTitleUnknown');
  }
}
trackTitleUnknown = Unknown Track

これらのアプローチは似ていますが、将来的に完全にローカライズ可能なHTMLツリーに向かって行くと、パターン1がデフォルトになる可能性があります。

両方のパターンでもHTML内では l10n-idl10n-args をセットしないのに注意します。なぜなら JavaScript で最初にトラックをロードした時だけにその値をセットするからです。なのでHTML 内でそれをセットするのはリソースの無駄です (l10n.js はそこに追加した値を翻訳しようとします)。

こうした属性を除去する時に、現在クリーンアップの魔法を使っていない事に注意するのも重要です。なので自分自身でクリーンアップする必要があります。これは将来変更される可能性があります。

多数の l10n 文字列を繰り返すコードを書く

多数の文字列を持つ場合(例えばエラーコード)、mozL10n.get を使用して、文字列の翻訳があるかをテストするとか、ない時にジェネリックな応答がセットされているかをテストするのにそそられます。これは良くないパターンで、なぜなら最初に mozL10n.get を使用し、次に見つからない文字列とそこにあるはずのない文字列とを混同して、再現が難しいバグやエッジケースとなるからです。

その代わりに、ローカライズされた文字列のリストを作ってそれをテストします、このように:

var l10nCodes = [
  'ERROR_MISSING',
  'ERROR_UNKNOWN',
  'ERROR_TIMEOUT'
];

if (l10nCodes.indexOf(code) !== -1) {
  elem.setAttribute('data-l10n-id', code);
} else {
  elem.setAttribute('data-l10n-id', 'ERROR_UNKNOWN');
}

L10nIDで動作するAPIを書く

もっと取り扱いが興味深いケースは、L10nID を受け取るAPIを書く時です。最も簡単なケースでは、このようになります:

function updateTitle(titleL10nId) {
  document.getElementById('titleElement').setAttribute('data-l10n-id', titleL10nId);
}

しかし上のケースに、l10nArgs や、ただの文字列や、HTML 断片の注入(将来的にはDOM オーバーレイで置き換える) さえも必要だったりする場合、推奨する完全なパターンはこちらです:

// titleL10n may be:
// a string -> l10nId
// an object -> {id: l10nId, args: l10nArgs}
// an object -> {raw: string}
// an object -> {html: string}
function updateTitle(titleL10n) {
  if (typeof(titleL10n) === 'string') {
    elem.setAttribute('data-l10n-id', titleL10n);
    return;
  }

  if (titleL10n.id) {
    navigator.mozL10n.setAttributes(elem, titleL10n.id, titleL10n.args);
    return;
  }

  if (titleL10n.raw) {
    elem.removeAttribute('data-l10n-id');
    elem.textContent = titleL10n.raw;
    return;
  }

  if (titleL10n.html) {
    elem.removeAttribute('data-l10n-id');
    elem.innerHTML = titleL10n.html;
    return;
  }
}

[1] あなたのコードが HTML 断片をサポートし、ノードが最初に {html: string} を受け取って、必ず後で l10nId ベースの翻訳に切り替わるフローをサポートする場合、textContentをきれいにする必要があります。そうしないと L10n.js が子ノードを持つノードへ l10nId をセットするのに文句を言うでしょう。詳細な背景はこの記事の"Untranslation"節を見て下さい。

もちろん、不要なケースをサポートする必要はありません。HTMLのケースが要るのはまれで、l10nArgs や生htmlも必要ないかもしれません。しかしl10nパラメータ用のスキーマに従うと、それを動作できるし、後になってもAPIを変更することなくこのケースのサポートを延長できます。

l10nId をセットをしないという現在の制限の結果の一つとして、あなたのAPIに古い翻訳があれば、DOM断片からそれをクリアするきれいな方法がない事があります。

テストする

テストを書く時、ノードの値をテストするのは反対です。そうすると他のロケールで無効なテストになり、非同期翻訳では動作せず、将来にDOM要素の表現法を変えた場合に動作しなくなります。

Gaia には共用の mock_l10n が提供されていて、これを使うべきです。

最高の戦略はテストを分割して、適切な l10n-id とl10n-args 属性をセットしているかを確認するか:

assert.equal(elem.getAttribute('data-l10n-id'), 'myExpectedId');

あるいはこうします:

var l10nAttrs = navigator.mozL10n.getAttributes(elem);
assert.equal(l10nAttrs.id, 'myExpectedId');
assert.deepEqual(l10nAttrs.args, {'name': 'John'});

通知API

ここで、通知を送りたい。W3C API では、title と body が文字列で渡されるのを期待します。Gaiaで提供されるのは、 mozL10n と共に動作する NotificationHelper です。

下記の代わりに:

var title = navigator.mozL10n.get('notification_title');
var body = navigator.mozL10n.get('notification_body', {user: "John"});

var notification = new Notification(title, {
  body: body,
});

// set onclick handler for the notification
notification.onclick = myCallback;

このように書くべきです:

NotificationHelper.send('notification_title', {
  bodyL10n: {id: 'notification_body', args: {user: "John"}}
}).then(function(notification) {
  notification.addEventListener('click', myCallback);
});

NotificationHelper は titleとbodyのL10n を、この記事の"Writing APIs that operate on L10nIDs" 節に書かれているのと同様に扱います。なので文字列またはオブジェクトを渡す事ができます。

ドキュメントのタグと貢献者

 このページの貢献者: chrisdavidmills, hamasaki, Uemmra3
 最終更新者: chrisdavidmills,