発明のための再発明

Webプログラマーが、プログラムの内部動作を通してプログラムを作る時の参考になるような情報を書くブログ(サーバーサイドやDevOpsメイン)

Dapperを読んだ

Dapperとは

googleが社内で利用している、分散トレーシングツール https://research.google.com/pubs/pub36356.html

この論文から出来たOSSとしてはtwitterによるzipkin(github)、uberによるjaeger(github)が有る。

内容

マイクロサービスだと1つのリクエストに対して複数のサービスが連携するので、一貫した記録が必要になる。 だから、リクエストの中でどのサービスにどれくらい時間がかかったかを記録したかった。 f:id:mrasu:20180401215816p:plain

各サービスはspanという開始と終了時間とリクエストid、annotationという任意のマークを持っている。 サービスが別サービスを呼び出すとspanは入れ子になる。

各spanは一度ローカルに保存され、その後中央(googleだから、bigtable)に送られる。 f:id:mrasu:20180401215825p:plain Dapperにはオーバーヘッドがあるので、サンプリングしている。 全部のリクエストを記録しなくても、トレーシングとしては、十分把握出来る

Amazonに見るi18n -「国際化対応」とは何を変える事か

サービスを海外展開したり日本にいる外国人にアピールしたくなった時、国際化対応が必要になります。
この記事を通して、「国際化対応したい」という話が出た時に「どこをどのように」対応することになるのか、対応範囲や見積などがなんとなく伝わればと思います。
以前の記事(i18nの書き方 from JavaScript)ではコードについて書きましたが、この記事ではAmazonを例に、開発者にとって国際化対応が「i18nのライブラリ入れるだけ」ではないこと、他にどのような対応が必要になるかという雰囲気を伝えられればと思います。

※国際化対応という言葉は色々な文脈で使用されますが、この記事では「現在想定している言語の話者以外(このブログを見ている人の大半にとっては日本人以外)にサービスを使用してもらうための対応一般」程度の雑な意味で使用します。

なぜAmazon

国際化対応には、大別して「言語を変える」という基本的な対応と「各文化に沿って表示やフローを変更する」という複雑な対応の2種類が存在します。

驚くべきことに、Amazonはこの2種類の対応を同時に行っているのです。
amazon.co.jpのフッターから、「言語選択」と「国の選択」ができるようになっています。
言語変更は別言語を使う人向けの対応であり、国の変更では販売商品やサービスなど大きく変わります。

下のURLがそれぞれのページです。

この仕組みのおかげで、国際化対応にあたり「どこが変わっているのか」、「なぜ妥協しているのか」を実感しやすいため、この記事ではAmazonを例としています。

※調査日は2018年3月19日

トップページ

まずは、トップページの国際化対応を見てみましょう

  • 日本用、日本語
    f:id:mrasu:20180331001348p:plain
  • 日本用、英語
    f:id:mrasu:20180331001343p:plain
  • 米国用、英語
    f:id:mrasu:20180331001204p:plain

ルーセル

まず目につくのは巨大なカルーセル部分(prime videoの宣伝画像)でしょうか。
ここでは、

という対応がされていることがわかります。つまり、プログラム的には大きな変更は行っていません
リンク先を画像毎に変えるというのはカルーセル実装時に対応していることであるので、開発にとっては「表示内容の変更」でしかありません。
また、トップページ全体を通してもナビゲーションの位置やレコメンドの配置場所など、レイアウトに大きな違いはないため、各国で共通したテンプレートを使用していることが伺えます。
つまり、言語や国などの条件によって受けの良いデザインを使用することで、各対象への受けを狙っているものの、あくまで部分的対応で実現しているということです。

このカルーセルのように、メイン画像を変えることは費用対効果が大きく、プログラムとしてもその場しのぎの簡単な対応で実現可能なので導入しやすい国際化対応となります。

部分的な表示・非表示

次に、最上部に米国用のみ「NEW&INTERESTING FINDS」と題した宣伝があるところを見ると、

  • 言語・地域によって表示するかしないか分かれる場所がある
  • この場所を表すdivタグ(id=nav-upnav)は日本用にも存在するが中のhtmlはなし
  • (米国用サイトであってもスペイン語では表示していない)

とわかります。カルーセルでは画像を変更していましたが、ここでは「表示・非表示」を通して表示内容を変更しています
非表示にするには、div部分だけ残して中身のhtmlを消すか、display:noneで消すか、divごとhtmlから消すかは悩みどころですが、好みの問題でしょう

さて、この対応が実施される典型は、「リンク先を隠す」という対応です。
「メインコンテンツについては国際化対応を行ったが、小さなコンテンツやセール、リンク先の別サービスでは対応していない」という時に、リンク先を隠すことで国際化対応していないコンテンツの存在を許容することが出来ます。
このような対応は移行時期だけではなく、サイト全体の国際化対応が完了した後にも、ターゲットの違いや費用対効果の面から、国際化対応しないと決定されるコンテンツがある限り存在し続けます。
そのため、プログラムを書く際は、表示非表示のロジックは共通化しておくのが良いでしょう。

レコメンド内容

トップページの分析の最後に商品の宣伝部分(おもちゃのレコメンドや"Deals recommended for you"以下)を見てみましょう

  • 日本用は言語が違ってもレコメンド内容は同一
  • 英語であっても日本語書籍を紹介するなど、言語差は考慮していない?
  • 米国用の"Deals recommended for you"は金額が出ていたり、カテゴリ横断の宣伝がされているなど大きく違う

ことがわかります。
つまり、日本サイトではログイン前に表示するレコメンドでは言語を意識していないのです。
推測ですが、ログインして使用するユーザーが多かったり、日本用サイトを見ているからには日本のコンテンツを理解できるはずだという想定をしたりしていて、差別化する必要性を強く感じていないのかもしれません。

まぁ、どのような理由にしろ、妥協可能だという判断がされたのでしょう。
国際化対応を考え始めるとあらゆる場所に対応しようと考えてしまいますが、妥協出来る部分は存在します。Amazonであっても妥協できる部分を上手く見つけて、妥協しているのです。
また、"Deals recommended for you"では金額を出していることから、部分的には差を出そうと努力していることもわかります。

他にもトップページには、メニュー項目が違ったり、英語表示なのに日本語広告があったり、中国大陸用サイトではテンプレートが違うなど、色々と見つかるので探してみると面白いものですが、長くなったのでここまでとして、商品画面を見てみましょう

商品画面

商品画面は下のような表示になっています(商品は洋書の「Refactoring」)

  • 日本用、日本語
    f:id:mrasu:20180331001518p:plain
  • 日本用、英語
    f:id:mrasu:20180331001522p:plain
  • 米国用、英語
    f:id:mrasu:20180331001524p:plain

URL

最初に注目するところはURLです。
Amazonは世界共通のDBを使用しているのか、同一商品に対しては同一idが付与されているようです。
たとえば、画像の例では「Refactoring」を使用していますが、日本と米国のURLは

と、idは共通しています。
同一商品には同一idが振られると使い回しが楽で良いのでしょう。
ただし、同一DBを使用しているかどうかははっきりしません。
日本特有の商品に対するidの振り方が違ったり、タイトルが微妙に違っていたりする所をみると、idのみ共通化し、各項目(商品名や著者、値段など)は各国が扱いやすいようにテーブルごと分けて管理しているかもしれません。
商品内容を各国が管理することで、DBの全内容を共有する必要がなくなり、各国が裁量を持ちやすくなるという利点が有ります。

ただ、DBやテーブルを別にするというのはかなり思い切った構成になるので、初期段階では同一データを保管しつつ、別表示が必要な項目のみを各言語・地域用に管理するのが無難でしょう。

レイアウト

画面構成についてみると、

  • 日本用の日本語と英語では言語が変わってるだけでレイアウトは同一
  • 米国用では「kindle,hardcover,paperback,other sellers」というタブで表示が分けられている

というように、明らかにレイアウトが別です。
htmlでみても、div(id=dp-container)の中が明らかに違っています。更に、dp-containerの中にjavascriptが埋め込まれていることから、dp-containerを使用するまでは共通のテンプレートが使用され、dp-container内で分けられているということが透けて見えます。

さらに興味深いことに、イタリアフランスAmazonでは日本と共通のレイアウトになっているようです。アメリカで先んじてレイアウトを変更しているようです。
Amazonではアメリカを中心に改良を行い、その成果を各国に配布するというフローなのもしれませんね。

このように、各国に合わせてレイアウトを別にすることで、各文化に対応した修正が可能になります。
しかし、その分翻訳者への伝達など調整コストが高くなってしまうので、日本用の日本語と英語が文章の翻訳のみ行っているようにレイアウトを変更しないという判断も可能です。

金額、在庫

最後に、日本用の英語と日本語での共通項目について見て、この記事を終わりたいと思います。
当たり前に感じますが、

  • 金額は共通(7,464円)
  • 在庫も共通(2点)
  • 配送予定日も共通(3/20)
  • レビューも共通

と、共通部分が多くあります。
外貨対応はクレジットカード任せなのか、ドルで購入できるなどはしていないようです(もしかすると、別に指定できる場所があるのかもしれませんが、あまり表に出していないよう)
日本に配達することは位置情報からわかっているので、在庫や配達予定日も同一のプログラムが動いているでしょう。
レビュー数も同じですし、レビューの内容は訳さずにそのまま表示しています。

他にも日本の商品のタイトルや内容紹介などは、英語で表示しても日本語が表示されます。大きく翻訳を割り切っていることがわかります。

このように、言語・地域に合わせて変更する必要が無い場所や、どうしても対応箇所が多くなってしまう場所では、割りきって翻訳せずに同一内容を表示するということも必要になります。

まとめ

以上、Amazonが行っている国際化対応について見てみました。
Amazonほどであっても全てを国・地域によって変更しているわけではないのです。
どこを対応して、どこを共通化するかを見極める必要が有ります。
金額や文言は別にするのか、どこの画像を変えるか、レイアウトを大きく変える価値があるページはどこなのか。
小さな変更と割りきって強引に対応するのか、DBを作り変えるなど根本的な対応を行うのか。
考える必要が有ります。

この記事が、「国際化対応」で何をする必要があるのか考えるヒントになればと思います。

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など各言語でデファクトとなっているフォーマットで保存すれば十分でしょう。