I18n ==== GNU gettext is used. Unlike many other i18n methods, gettext supports plural forms. .. image:: ../img/poedit.png Write internationalized messages in source code ----------------------------------------------- ``xitrum.Action`` extends ``xitrum.I18n``, which has these methods: :: 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) In a action or action, just call them. In other places like models, you need to pass the current action to them and call ``t`` and ``tc`` on it: :: // In an action respondText(MyModel.hello(this)) // In the model import xitrum.I18n object MyModel { def hello(i18n: I18n) = i18n.t("Hello World") } Extract messages to pot files ----------------------------- Create an empty i18n.pot file in your project's root directory, then recompile the whole project. :: sbt/sbt clean rm i18n.pot touch i18n.pot sbt/sbt compile ``sbt/sbt clean`` is to delete all .class files, forcing SBT to recompile the whole project. Because after ``sbt/sbt clean``, SBT will try to redownload all :doc:`dependencies `, you can do a little faster with the command ``find target -name *.class -delete``, which deletes all .class files in the ``target`` directory. After the recompilation, i18n.pot will be filled with gettext messages extracted from the source code. To do this magic, `Scala compiler plugin technique `_ is used. One caveat of this method is that only gettext messages in Scala source code files are extracted. If you have Java files, you may want to extract manually using ``xgettext`` command line: :: 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") Then you manually merge i18n_java.pot to i18n.pot. Where to save po files ---------------------- i18n.pot is the template file. You need to copy it to .po files and translate. Xitrum monitors directories named ``i18n`` in classpath. If a .po file in that directory is updated or added at runtime, Xitrum will automatically reload that .po file. :: src main scala view resources i18n ja.po vi.po ... Use a tool like `Poedit `_ to edit po files. You can use it to merge newly created pot file to existing po files. .. image:: ../img/update_from_pot.png You can package po files in multiple JAR files. Xitrum will automatically merge them when running. :: mylib.jar i18n ja.po vi.po ... another.jar i18n ja.po vi.po ... Set language ------------ * To get languages set in the ``Accept-Language`` request header by the browser, call ``browserLanguages``. The result is sorted by priority set by the brower, from high to low. * The default language is "en". To set language for the current action, for example Japanese, call ``language = "ja"``. * To autoset the most suitable language in resources, call ``autosetLanguage(availableLanguages)``, where ``availableLanguages`` is a list of languages corresponding to .po files in ``resources/i18n`` directory and JAR files. If there's no suitable language, the language is still the default "en". * To get the current language set above, use ``language``. In your action, typically in a before filter, to set language: :: beforeFilter { val lango: Option[String] = yourMethodToGetUserPreferenceLanguageInSession() lango match { case None => autosetLanguage(Locale.forLanguageTag("ja"), Locale.forLanguageTag("vi")) case Some(lang) => language = lang } } Validation messages ------------------- jQuery Validation plugin provides `i18n error messages `_. Xitrum automatically include the message file corresponding to the current language. For server side default validators in ``xitrum.validator`` package, Xitrum also provide translation for them. Plural forms ------------ :: tn("Message", "Plural form", n) tcn("Context", "Message", "Plural form", n) Xitrum can only work correctly with Plural-Forms exactly listed at: * `What are plural forms `_ * `Translating plural forms `_ Your plural forms must be exactly one of the following: :: 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 Date and number format ---------------------- If you use Scalate template engine, by default the date and number format will be the format of the language of the current action. If you want to use other format: :: import java.text.{DateFormat, NumberFormat} val myDateFormat = ... val myNumberFormat = ... val options = Map("date" -> myDateFormat, "number" -> myNumberFormat) respondView(options)