Kaizen AdにおけるSPAでのi18nへの取り組みと手法について

こんにちは。フロントエンドエンジニアのaxrossです。この記事では、弊社のサービス「Kaizen Ad」で行った "i18n" (国際化) の取り組みについて紹介したいと思います。

Kaizen Adでは、サービス提供を行う地域として日本とアメリカを同時に見据えて開発をしてきました。そのため、プロジェクト発足当初から意識して開発を始め、(クローズドな) ファーストローンチの2週間後にはこの記事で紹介するようなi18nを取り入れています。

i18nとは "Internationalization" の略で、単語の最初の文字 "I" と 単語の最後の文字 "N" との間に18文字あることからこのように略されます。

i18nをどのように考えるか?

まずは、Kaizen Adを国際化するにあたって考えたことについて話したいと思います。大きく分けて2つの点をチームで徹底して意識しながら開発を進めました。

「ラベルの翻訳」と「翻訳されたデータ」

まず1つは、「翻訳されたデータ」と「ラベルの翻訳」を完全に別のものとして考えるように徹底したという点です。

f:id:kaizenplatform:20180629110825p:plain

これは開発中の「Kaizen Ad」の画面ですが、この画面内に存在する「翻訳したいモノ」は以下のように分類できます。

f:id:kaizenplatform:20180629110838p:plain

青い部分はアプリケーションにとって静的で、つまり「もし国際化対応を配慮しない場合はUIのコードの一部として予め埋め込みが可能な部分」です。対して赤い部分は管理画面から編集が可能な項目です。この青い部分を僕たちは「ラベル」、赤い部分は「データ」と呼び分けています。

ラベルの翻訳はクライアントサイドの責務です。今はWebのUIしかありませんが、同じAPIサーバーを使う他のクライアントアプリケーション (iOS, Android, Desktopなど) を今後開発するにあたっては、同様の項目でも操作文脈に対して適切な、微妙に違ったラベルが割り当てられる可能性があるためです。

データに関しては、APIサーバーが提供する時点で翻訳されているべきだと考えています。今後どんなクライアントアプリケーションを開発するにあたっても不変ですし、そもそもレコードとしてデータストアに蓄積されるものをクライアントサイドが翻訳することは責務として適切ではないどころか、現実的には不可能です。

"Language" と "Locale"

もう1つは、 "Language" と "Locale" を明確に分けて考えるようにしているということです。これは en-USja-JP のようなIETFの「Language tag」などで一般的な考え方です。 en の部分はLanguage (言語) を示し、 US の部分は Locale (地域) を示します。

言語が同じでも地域によって日付などの慣習的な表記が微妙に違うことがあります。たとえば、アメリカは MM/DD/YYYY 、カナダは YYYY-MM-DD 、オーストラリアは DD/MM/YYYY などとそれぞれ違います (もっとも、この3国は言語の面でも多少の差異がありますが) 。

実際のアプリケーションでは日時や通貨、数値のフォーマットやカレンダーなど、Localeによる差異がたくさんあり、これらはLanguageとは関連しません。これらをユーザーの設定としてデータストアやアプリケーションの値として保持する際には、この2つに分けて管理するのが適切だということです。

i18nをどのように実装すべきか?

次に、それらの考え方を実装においてどのように当てはめて開発していったかをお話します。

ラベルの翻訳: メッセージをビューで変換する

ラベルの翻訳に関してはFormatJSという規格の実装である 📦react-intl を利用しました。react-intlはPropsとしてキーを渡すことで翻訳結果となる文字列が返るReact Componentを提供するライブラリです。

<FormattedMessage id="headbar.projects" />  // => "プロジェクト一覧" | "Projects"

ここで id というPropsに渡している文字列は翻訳辞書のキーです。Kaizen Adは日本語と英語に対応していますが、それぞれのテキストはJSONの翻訳辞書としてサーブし、クライアントアプリケーションで利用して翻訳しています。

f:id:kaizenplatform:20180629110852p:plain

この翻訳辞書のファイルはユーザーの設定に応じてHTTPでフェッチして読み込み、アプリケーションの状態値として扱いReactのContext APIを経由して末端React Component (<FormattedMessage>) から読み取って表示しています。

f:id:kaizenplatform:20180629110901p:plain

FormatJSでは翻訳辞書の値に変数を入れて使うこともできます。以下は変数を用いて「データ」と「ラベル」を組み合わせて画面に表示している例です。

<FormattedMessage
  id="project_types_list_page.filter.tags.any_of_group"
  values={{ tagGroup: tagGroup.label }}
/>

アンチパターン: ビュー以外の層で翻訳する

アンチパターンとして学んだのは、この「ラベルの翻訳」をビュー以外の層で行ってしまうと、後述の「翻訳されたデータ」のような取り回しが必要になってしまうということです。ビューからは「翻訳されたテキスト」という漠然とした文字列であることしか認識できず、コードの読解やテストの面で不利になります。

上記のような <FormatedMessage ...> は、コードリーディングにおいては「そこに翻訳されるラベルがある」というプレースホルダとしても機能します。ビューからは翻訳辞書のどのキーの値が欲しいかという明示のみに済ませるのが効果的です。

"翻訳された" データ

管理画面で編集が可能なデータは、それぞれの言語でどのように表示するかを管理画面で編集できるようにしています。

f:id:kaizenplatform:20180629110910p:plain

こう聞くと「あらゆるレコードに対して同じようなことをしなければならないのか」と思うかもしれませんが、実際に翻訳が必要なものはごく少数で済んでいます。また、ユーザーが入力した情報 (トランザクションデータ) はそもそも翻訳することができないので、「翻訳されたデータ」となるのはマスターデータに限られます。

最初の画像にあった画面にある「タグ」の仕組みを管理するエンティティに限って図にすると、下記のような形でしょうか。 f:id:kaizenplatform:20180629110919p:plain

クライアントアプリケーションがこのデータを取得する時は、オプションとして「どの言語で欲しいか」を付与して呼び出しています。下記は取得を行っているGraphQLのクエリです。

query($language: ID!, ... 略 ...) {
  projectTypeTagGroups(language: $language, ... 略 ...) {
    ... 略 ...
  }
}

i18nをどのように運用していくか?

ネイティブスピーカーの力を借りる

その国にとっての外国人が翻訳辞書の文言を決めることには問題があります。表現は言語に特定した背景に関連することが多いからです。しかし幸運にも弊社にはアメリカ支社とそこに努めるネイティブスピーカーがおり、彼ら/彼女らの協力を得ることができています。

たとえば「表示する」という言葉1つとっても、それを表現する "display", "indicate", "show" などの様々な単語が存在します。ネイティブスピーカーは、「こういうケースではこの言葉が使われるのが『普通』」という「普通」を感覚で知っていますが、僕のような外国人が言葉を選択すると安易に "show" などを選んでしまいそうです。

「意味のある仮の文言」を当てはめる

日本人の開発メンバーがUIを開発する際には、一旦「意味のある仮の文言」、つまり「間違っていても構わないのでできるだけ伝わるような言葉」を当てはめ、その後アプリケーションのことをよく知るネイティブスピーカーに修正を依頼します。

最初から正解を目指すことはできませんが、かと言ってそのUI部品が何を表現するかが読み取れないと正しい翻訳が行えません。仮の文言とは言っても、そのUI部品を通じて何を知らせるか (クリックすると何が起きるかなど) を最低限伝えられるような文言を当てはめる必要があります。

in-context編集によるレビュー

上で画像にあったような翻訳辞書ファイルとにらめっこしながら修正をするのは骨が折れることです。言葉だけではUI部品の形や操作の文脈が想像できず不適切な言葉に修正してしまうかもしれませんし、同じようで少し違う文言が複数ある場合には修正箇所を間違えてしまうことも考えられます。

そのため、我々のチームでは翻訳辞書のストア先としてPhraseAppというSaaSを利用し、PhraseAppの提供している機能の1つである「in-contextエディタ」を活用しています。in-contextエディタとは、実際のアプリケーションの画面に翻訳辞書のエディタがオーバーレイ表示され、そのエディタの上で文言を変更できるというものです。

f:id:kaizenplatform:20180629110929p:plain

特殊な開発オプションとともにKaizen Adを開くと、in-contextエディタが起動します。in-contextエディタの起動中はすべてのラベルに編集アイコンが表示され、それをクリックするだけでテキストを実際の画面への反映を確認しながら編集することができます。これは修正を行うメンバーだけでなく、UX面から細かいフレーズの変更を行うデザイナーからも好評です。

最後に

この記事では「Kaizen Ad」でのi18nへの取り組みについてご紹介しました。

こういった課題への取り組みはコードでの話に閉じてしまいがちですが、ハイスピードな開発と生産性の向上を両立させるにはチームメンバーの体験 (Developer Experience) を改善しようとコード以外での努力をすることも大事だと考えています (ここでの「Developer」とはコードを書く人間だけに限りません) 。