We use ICU format messages to translate our large app at Featurespace. Here’s how:
Even before internationalising the app, we kept all our string literals separate from the pages, in modules called I18n.elm
. We kept those modules but, instead of strings, they now export Translatable values. Every translatable has a key, and default message in English:
sorting : Translatable
sorting =
"Sorting"
|> i18n.key "sorting"
|> i18n.translate
They may also include a maximum length and a comment to the translator, like this:
sortingSecondary : Translatable
sortingSecondary =
"sec"
|> i18n.key "sortingSecondary"
|> i18n.comment "Very restricted space"
|> i18n.maxLength 3
|> i18n.translate
We keep key values unique within an I18n module. In the above examples, i18n
is a record instantiated in the module like this:
module Page.Table.I18n exposing (sorting, sortingSecondary)
import Helper.Translate as Translate exposing (Translatable)
i18n : Translate.Functions
i18n =
Translate.init "Page.Table"
The function i18n.key
prepends the module scope identifier, so the full key value of the translatable sortingSecondary
is “Page.Table.sortingSecondary”.
To include a translatable message in a page we need a dictionary from keys to messages in the required language which we fetch from the server at load time and keep in a session object together with the currently selected language.
secondaryTitle : Session -> Html msg
secondaryTitle session =
I18n.sortingSecondary
|> Translate.translate session
|> Html.text
This looks up the fully qualified key sortingSecondary
in the currently selected dictionary and returns the message text. If it can’t find the key in the dictionary it will return the English message directly from the translatable. So we don’t need an English translation file: an empty dictionary is enough to develop and test the app.
Plurals
We write plurals in ICU message format, like this:
results : TranslatableWithPlural
results =
"{count, plural, one {{count} result} other {{count} results}}"
|> i18n.key "results"
|> i18n.translatePlural
To use these we provide the value for the {count}
variable (here resultCount
):
Translate.translatePlural session I18n.results resultCount []
The []
at the end is an empty list of key value pairs to substitute for other {key]
variables.
In addition to keys and translated messages, our language files include a rule to derive plural groups (‘one’, ‘two’, ‘few’,‘many’, ‘other’) from numbers. When we decode the language file we put the keys and messages into a dictionary and parse the plural rule to give an Elm function we can use to chose the right part of the message text.
Translating
When we’re ready to translate we run a script that compiles all the I18n
modules and, in a similar way to the node elm-test runner, builds an elm module that imports them and extracts the keys, messages, comments and maximum length values. This module gets executed on Node and its output becomes the source files for our translation company. The translation company we’re using at the moment wants Gettext PO files, so we convert the ICU messages to PO!
When we receive the translated PO files we run another script that converts them to JSON with messages in ICU format, and integrates them with the language definitions (which include the plural rules). The resulting JSON files are the resources that our app fetches and decodes to get the language dictionaries to translate messages. We write an ICU message format parser but its not (yet) a separate package.