国際化

Play framework + React サイトの i18n

概要

タイトルからある程度想像はつくと思うけど、一応書く。

前提

  • Play! framework の messages ファイルとかを使って、テキストの i18n を行っている。ドキュメントはこの辺
  • 日付とか、その辺の i18n は今のところやっていない

やりたい事

  • React で表示しているメッセージも多言語化したい
  • Play 用、React 用と、ファイルを2セット用意するのは避けたい

環境

  • Play! framework 2.5
  • React 15

結論 (tl;dr)

  • Play JsMessages というライブラリを使用し、messages ファイルの内容を JavaScript のオブジェクトとして出力するエンドポイントを作成 (/i18njs/allMessages という名前にした)
  • Play! framework のビューの中で <script> タグを使ってそのJSオブジェクトを読み込む
  • i18next というライブラリに、その JS オブジェクトを渡す
  • React (やその他 JS コード)でテキストを出力する部分で、i18next を使う

検討したけどボツになった方法

messages ファイルを、gulp タスクで JS ファイルに変換

一番最初は、 gulp-props 等を使った gulp タスクで messages ファイルを JS ファイルに変換して、それを読み込んで使おうと思った。

ただ、messages ファイルは、Java のプロパティファイルと若干仕様が異なるので、うまくいかないケースがあった。具体的には、メッセージ中に \n が含まれているとダメだった。

gettext の po ファイルを使ったり

messages ファイルの内容を po ファイルに移行し、何らかの方法で messages ファイルと JS 用のファイルを生成する、なんて方法も考えられるけど、po ファイルに移行するのが面倒だし、そもそも po ファイルを messages ファイルに変換する方法もあるかどうかあまり調べてないので、ボツ。

ちなみに、Play! framework は、何でこんな中途半端な i18n 機能をわざわざ作ったんだろうか。pluralization にも対応してないし。

やったこと(詳細)

Play JsMessages のインストール

ドキュメント見て下さい。

messages の内容を JS として出力するエンドポイント作成

言葉だと分かりにくいのでコードで。

コントローラーを作成。

import jsmessages.JsMessagesFactoryComponents
import play.api.mvc.{ Action, Controller }
import play.api.i18n.{ Messages, I18nSupport, MessagesApi }

class I18nJsController @Inject() (val messagesApi: MessagesApi)
  extends Controller with I18nSupport with JsMessagesFactoryComponents {

  private val jsMessages = jsMessagesFactory.all
  // 以下のような JS が出力される
  // i18n.jsMessages = ...
  private val namespace = Some("i18n.jsMessages")

  // 全ての言語の messages の中身が出力される
  val allMessages = Action { implicit request =>
    Ok(jsMessages.all(namespace))
  }

  // 現在の言語のみ(言語判定は、Play の通常の仕組み)
  // 結果的に、こちらは使ってないけど、参考までに
  def messages() = Action { implicit request =>
    Ok(jsMessages(namespace))
  }
}

routes ファイルはこんな感じ。エンドポイント名は適当。

GET /i18njs/allMessages @controllers.I18nJsController.allMessages()
GET /i18njs/messages    @controllers.I18nJsController.messages()

ビューで JS を読み込む

以下のようなビュー(パーシャル)を作って、他のビュー(レイアウト)から読み込んでる。

@()(implicit messages: Messages)

<script>
  var i18n = i18n || {};
  // 現在のブラウザの言語を、JSの変数として設定しておく
  i18n.lang = '@messages.lang.language';
</script>

<!-- messages ファイルの中身を JS オブジェクトにしたもの -->
<script src="@croutes.I18nJsController.allMessages"></script>
<!-- React コンポーネントとかをまとめて minify したもの -->
<script src="@routes.Assets.at("javascripts/script.min.js")"></script>

React 側で、i18next にJS 版の messages を渡す

React 側で、以下のような i18next のラッパーを作成した。詳細は、i18next のドキュメントを参照。

import i18next from 'i18next';

var tmpResources = {};
// ※1
for(var lang in i18n.jsMessages.messages) {
    if (lang != 'default' && lang != 'default.play') {
        var item = i18n.jsMessages.messages[lang];
        tmpResources[lang] = {translation: item};
    }
}

i18next
    .init({
        lng: i18n.lang,
        fallbackLng: 'en',
        debug: true,
        resources: tmpResources,

        // The default is '.'
        // ※2
        keySeparator: '/',

        // ※3
        interpolation: {
            escapeValue: false, // not needed for react!!
            prefix: '{',
            suffix: '}',
        },

        // react i18next special options (optional)
        react: {
            wait: false, // set to true if you like to wait for loaded in every translated hoc
            nsMode: 'default' // set it to fallback to let passed namespaces to translated hoc act as fallbacks
        }
    });

export default i18next;
  • ※1: i18n.jsMessages は、 /i18n/allMessages エンドポイントで出力されたもの。それを、i18next に渡せる形に変形する。
  • ※2: Play の messages ファイルだと、”page1.header.hello=こんにちは” みたいなエントリーは一般的だが、キー名に “.” が含まれると、そこが i18next のネームスペースの区切りとみなされるので、 “.” 以外を指定する必要がある
  • ※3: messages ファイルだと {0}, {1}, … {n} というのが、他の文字列に置換されるが、i18next のデフォルトだと、{{xxx}} という形式が使われるので。

React でテキスト表示する部分を書き換える

これでようやく準備が整った。後は、以下のように使う。

import i18n from './util/i18n'
//中略
// 「こんにちは、山田太郎さん」
<h1>{i18n.t('page1.header.hello', ['山田太郎'])}</h1>

//後略

まとめ

Play JsMessages Library と i18next を使って、Play! framework + React アプリのメッセージの i18n を比較的簡単に行う事が出来る。

 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です