I18n

GNU gettext を使用します。gettextは他の国際化方法と異なり、複数形をサポートしています。

_images/poedit.png

ソースコード内への国際化メッセージの記載

xitrum.Actionxitrum.I18n を継承しており以下の2つのメソッドを持ちます:

t("Message")
tc("Context", "Message")

t("Hello %s").format("World")

// 1$ and 2$ are placeholders
t("%1$s says hello to %2$s, then %2$s says hello back to %1$s").format("Bill", "Hillary")

// {0} and {1} are placeholders
java.text.MessageFormat.format(t("{0} says hello to {1}, then {1} says hello back to {0}"), "Bill", "Hillary")

t("%,.3f").format(1234.5678)                                // => 1,234.568
t("%,.3f").formatLocal(java.util.Locale.FRENCH, 1234.5678)  // => 1 234,568
// Above, you explicitly specify locale.
// If you want to implicitly use locale of the current action:
// when English => 1,234.568, when French => 1 234,568
t("%,.3f", 1234.5678)

actionの中では、それらのメソッドを直接呼び出すことができます。 modelのようにaction以外の場所では、xitrum.I18n オブジェクトをインポートし、 t または tc メソッドを呼び出します:

// In an action
respondText(MyModel.hello(this))

// In the model
import xitrum.I18n
object MyModel {
  def hello(i18n: I18n) = i18n.t("Hello World")
}

potファイルへのメッセージの展開

空のi18n.potファイルをプロジェクトのルートディレクトリに作成し、 プロジェクト全体を再コンパイルします。

sbt/sbt clean
rm i18n.pot
touch i18n.pot
sbt/sbt compile

sbt/sbt clean で全ての.classファイルを削除し、SBTにプロジェクト全体の再コンパイルを実施します。 sbt/sbt clean の後、SBTはコンパイル時に全ての 依存ライブラリ を再ダウンロードを行いますので、 より時間を節約するには find target -name *.class -delete と実施することで 同じように target ディレクトリ内の.classファイルを削除することができます。

リコンパイル実施後、ソースコードから抽出されたメッセージがi18n.potファイルにgettext形式で出力されます。 この魔法のような動作は Scala compiler plugin technique により実現されています。

ただし一つ注意点があります。このメソッドはScalaのコードからのみメッセージを抽出します。 もしプロジェクト内にJavaファイルがある場合、 xgettext コマンドを使用してメッセージを抽出します:

xgettext -kt -ktc:1c,2 -ktn:1,2 -ktcn:1c,2,3 -o i18n_java.pot --from-code=UTF-8 $(find src/main/java -name "*.java")

出力されたi18n_java.potはi18n.potにマージする必要があります。

po ファイルの保存先

i18n.potはテンプレートであるため、各言語に対応させるにはi18n.potファイルをコピーして、<language>.po として保存し翻訳を開始します。

Xitrumはクラスパス中の i18n という名前のディレクトリを監視します。 もしそのディレクトリ内の <language>.po ファイルに変更があった場合 Xitrumは自動的に <language>.po ファイルをリロードします。

src
  main
    scala
    view
    resources
      i18n
        ja.po
        vi.po
        ...

poファイルを編集やマージには Poedit のようなツールを使用することができます。

_images/update_from_pot.png

poファイルは複数のJARに含めることができ、Xitrumはそれらを自動的にマージします。

mylib.jar
  i18n
    ja.po
    vi.po
        ...

another.jar
  i18n
    ja.po
    vi.po
        ...

言語の設定

  • ブラウザからのリクエストに含まれる Accept-Language リクエストヘッダーを取得するには、 browserLanguages を実行します。結果はブラウザによって送信された優先順位の高い順にソートされて取得できます。
  • デフォルト値は “en” です。現在の言語を日本語に変更するには、 language = "ja" と実行します。
  • 適切な言語を言語リソースから自動でセットするには autosetLanguage(availableLanguages) を実行します。 availableLanguagesresources/i18n ディレクトリーとJARファイル内に含まれる言語リソースのリストを指定します。 もし指定された言語リソースが存在しない場合、言語設定は”en”が使用されます。
  • 設定された言語を確認するには、language 変数にセットされた値を参照します。

一般的にアクションではビフォアフィルターにおいて言語を設定します:

beforeFilter {
  val lango: Option[String] = yourMethodToGetUserPreferenceLanguageInSession()
  lango match {
    case None       => autosetLanguage(Locale.forLanguageTag("ja"), Locale.forLanguageTag("vi"))
    case Some(lang) => language = lang
  }
}

バリデーションメッセージ

jQuery Validation プラグインは i18n error messages を提供しています。 Xitrumは現在の言語に対応するメッセージファイルを自動的にインポートします。

xitrum.validator パッケージが提供するサーバサイドバリデーションにおいても、 Xitrumはそれらの翻訳を提供しています。

複数形への対応

tn("Message", "Plural form", n)
tcn("Context", "Message", "Plural form", n)

Xitrumは以下の仕様に沿って複数形の単語を翻訳します。

複数形の単語は以下のいずれかの書式に従う必要があります:

nplurals=1; plural=0
nplurals=2; plural=n != 1
nplurals=2; plural=n>1
nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2
nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2
nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2
nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2
nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2
nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2
nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2
nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3

日付と数値のフォーマット

もしScalateテンプレートエンジンを使用している場合、日付と数値のフォーマットは現在のアクションの言語設定に従うことになります。

異なるフォーマットを使用する場合:

import java.text.{DateFormat, NumberFormat}

val myDateFormat   = ...
val myNumberFormat = ...
val options        = Map("date" -> myDateFormat, "number" -> myNumberFormat)
respondView(options)