I18n¶
GNU gettext is used. Unlike many other i18n methods, gettext supports plural forms.
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 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 <language>.po files and translate.
Xitrum monitors directories named i18n
in classpath.
If a <language>.po file in that directory is updated or added at runtime,
Xitrum will automatically reload that <language>.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.
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, callbrowserLanguages
. 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)
, whereavailableLanguages
is a list of languages corresponding to .po files inresources/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:
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)