Guidelines for Internationalization
Table of Contents
- i18n implementation
- Gettext usage
- Translating ManageIQ
- Updating translations
- Fast Forward of translations
i18n implementation
ManageIQ uses gettext for internationalization. In particular, the project uses the following gems to make internationalization work:
Gettext usage
ManageIQ supports internationalization of strings in ruby and haml sources as well as JavaScript.
Ruby / HAML
The following gettext routines are supported in Ruby and HAML sources:
_(msgid)
– translatesmsgid
and returns the translated stringN_(msgid)
– dynamic translation: this will putmsgid
in the gettext catalog but the string will not be translated by gettext at this time (see delayed translation below)n_(msgid, msgid_plural, n)
– returns either translatedmsgid
or its translated plural form (msgid_plural
) depending onn
, a number determining the count (i.e. number above 1 means plural form)s_(msgid, seperator = "|")
– translatesmsgid
, but if there are no localized text, it returns a last part ofmsgid
separated byseparator
(|
by default)ns_(msgid, msgid_plural, n, seperator = "|")
– similar to then_()
, but if there is no localized text, it returns a last part ofmsgid
separated byseparator
.
JavaScript
Internationalization in JavaScript is done with gettext_i18n_rails_js
gem. The gem extends gettext_i18n_rails
making the .po
files available to client side javascript as JSON files.
Unlike Ruby / HAML sources, JavaScript uses __()
(i.e. two underscores rather than one) to invoke gettext. This is done to avoid conflicts with other JavaScript modules.
The gettext routines supported in JavaScript sources:
__(msgid)
n__(msgid, msgid_plural, n)
s_(msgid)
The semantic is similar to the Ruby equivalents.
Angular applications
Purely Angular applications, such as Self-Service UI and ui-components, use angular-gettext for internationalization.
The angular-gettext
module allows for annotating parts of the Angular application (HTML of JavaScript), which need to be translated.
Guides for annotating content:
Note, that in Self-Service UI, thanks to:
we can use N_()
and __()
in javascript sources instead of regular angular-gettext
routines (gettext
and gettextCatalog.getString
).
Substitute filter
In certain situations, it’s not possible to correctly annotate strings for translation when these strings are a value of an HTML element and contain a variable interpolation. For example:
<span tooltip="{{item.v_total_vms}} VMs">
This is where the substitute
filter comes in handy. The above situation would then be resolved as:
<span tooltip="{{ '[[number]] VMs'|translate|substitute:{number: item.v_total_vms} }}">
i.e. [[
and ]]
mark start and end of a variable to be substituted, the actual values would be
then substituted with the context of the substitute
filter.
Caveats
There are certain aspects of using angular-gettext
you should be aware of.
- Be careful where you put the
translate
directive. For example, a markup like:
<div class="outside" translate>
<div class="inside">
<span>Text</span>
</div>
</div>
will result in the whole
<div class="inside">
<span>Text</span>
</div>
block being collected during string extraction by yarn run gettext:extract
. Correctly, the translate
directive should be placed inside the span
element.
- Don’t use dynamic content inside
__()
. For example, the following javascript code won’t be collected correctly byyarn run gettext:extract
.
s = __(sprintf("My name is %s.", me.name));
The above should correctly be:
s = sprintf(__("My name is %s."), me.name);
- Don’t apply the
translate
filter on a dynamic content. The string inside would not be correctly extracted during string collection. For example the following won’t be collected correctly byyarn run gettext:extract
.
<span>
{{ ((magicVariable != null) ? "It's there" : "It's not there") | translate }}
</span>
The above should correctly be:
<span>
<span ng=if="magicVaiable" translate>It's there</span>
<span ng=if="!magicVariable" translate>It's not there</span>
</span>
- Avoid concatenating English strings. For example, a javascript code like:
if (action.name == "create") {
var verb = "created";
} else {
var verb = "updated";
}
message = sprintf(__("Item was %s"), verb);
would contain mixed languages when shown in non-English locale.
Correctly, the code should read:
if (action.name == "create") {
message = __("Item was created");
} else {
message = __("Item was updated");
}
Delayed / dynamic translations
Sometimes we need to delay translation of a string to a later time. For example menu items and sections or certain tree nodes are defined once but then need to be rendered many times possibly in different locales.
In the simplest case you can use N_('bar')
saying “do not translate this string” such as:
menu_item = Menu::Item.new(N_('Cloud Tenants'))
and such strings will be caught by the rake gettext:find
task so these strings end up in the catalog.
Then you do the translation when needed (e.g. when generating the HTML or JSON format of the data) by calling _()
such as:
menu_item_text = _(menu_item.text)
To properly internationalize a string with interpolation that needs to be translated at render time rather than right away, use PostponedTranslation
class.
tip = PostponedTranslation.new( _N("%s Alert Profiles") ) { "already translated string" }
and then in the place where the value is used you will have:
translated_tip = tip.kind_of?(Proc) ? tip.call : _(tip)
String interpolation
Whenever you need to use string interpolation inside a gettext call, you need to follow several rules.
- different languages might have different word orders so we use named placeholders:
_("%{model} \"%{name}\" was added") % {:model => ui_lookup(:model=>"MiqReport"), :name => @rpt.name}
These forms are not acceptable:
_("%s (All %s)" % [@ems_cluster.name, title_for_hosts]) # the name of the placeholder can provide vital information to the translator
_("No %s were selected to move up") % "fields" # use named placeholder even in the case of a single placeholder
- do not use variables inside gettext strings as these will not be extracted and placed in the gettext catalog
Incorrect:
text = "Some random text"
_(text)
Correct:
_("Some random text")
Marking gettext strings in UI
To be able to see strings which pass through gettext (i.e. are translatable), you have to add the following to the servers advanced settings:
ui:
mark_translated_strings: true
and restart the ManageIQ application. With these settings on, all strings passing through gettext will be marked with »
and «
markers:
»webui text that went through some of the gettext routines«
Translating ManageIQ
To contribute with translation to ManageIQ, create an account at Transifex and navigate to the ManageIQ project page
Translator notes
- How do I translate keys in the form of
Hardware|Usage
? What do they mean?Hardware|Usage
means Usage in namespace Hardware. This is the way we translate model attributes for ActiveRecord models for example. You do not have to translate “Hardware”, just translate “Usage”.
- What does the key
locale_name
mean?locale_name
means name of the given language in the language itself. Such as “Deutsch” for German or “Česky” for Czech. Make sure to provide the value for this key, without it the language/locale cannot be presented in the UI.
Updating translations (developers)
The general workflow for updating translations is:
- extract strings from source code and create new gettext catalog.
- upload the new catalog into a translation tool (we use Transifex).
- once the translations for the languages are complete, fetch the translations from the translation tool and put them into git.
The instructions will differ in details depending on the specific ManageIQ project.
Transifex
We use Transifex for online translations. We maintain several transifex projects for the ManageIQ project:
To be able to use transifex from command line, make sure you have transifex-client
installed and configured (documentation)
To be able to manipulate transifex catalogs, you need to be maintainer of the project in transifex.
ManageIQ (the main project)
Steps for updating translations:
- Update message catalog in the core ManageIQ repository. This is done to make sure the message catalog contains up to date (i.e. current) strings for translators.
To update the message catalog including the plugin repos, run the following rake task in the root of
ManageIQ/manageiq
git checkout:
$ bundle exec rake locale:update_all
This task will:
- extract model attributes
- extract strings from
en.yml
- extract strings from other yaml files
- extract strings from ruby, javascript and haml sources
- extract strings from all ManageIQ plugins, including node plugins (react-ui-components and ui-components)
Now, commit and push changed files into git (branch, pull request, etc.). Although all the locale/*/*.po
files will be updated by this step, make sure that only the following file is committed:
locale/manageiq.pot
- Upload the catalog created in previous step to Transifex:
$ cd locale
$ tx push --source
Now translators in Transifex will have the latest stuff to translate available.
- Once the translators finish translations of a particular language, pull the translated catalog from Transifex back to ManageIQ repository.
Run the following in ManageIQ git checkout:
$ cd locale
$ tx pull --all # use --language option, if you wish to download only specific locales
$ bundle exec rake locale:po_to_json
If you are adding new locales / languages to ManageIQ, don’t forget to create yaml file containing localized names of each included language. From the root of ManageIQ checkout, run the following:
$ bundle exec rake locale:extract_locale_names
This will update config/human_locale_names.yaml
file.
Now commit and push the changes (branch, pull request, etc.). Make sure that in core ManageIQ you commit the following files:
locale/*/*.po
config/human_locale_names.yaml # optionally, if you added locales
and in ManageIQ UI Classic repository, commit the following files:
app/assets/javascripts/locale/*/*.js
app/javascript/packs/bootstrap-datepicker-languages.js # optionally, if the file changed
ManageIQ Self Service UI
- Update the gettext catalogs:
$ cd client
$ yarn run gettext:extract
- Upload the catalogs to Transifex:
$ tx push --source
- Once the translations are complete, download the translated catalogs:
$ tx pull --all # use --language option, if you wish to download only specific locales
- Convert the translated .po catalogs into .json files:
$ yarn run gettext:compile
- Commit and push the new content into git:
$ git commit client/gettext/po client/gettext/json
$ git push
Fast-forwarding translations from release branch to master branch
Ordinarily, translations for ManageIQ are done for particular release and the strings for translations are therefore taken from a release branch. When the translation work for the release branch is done, it is a good practice to fast-forward the translations from the release branch to the master branch. One way to do the fast-forward is to utilize Transifex. The following example assumes fast forward from jansa branch to master:
- Run
bundle exec rake locale:update_all
in ManageIQ core on master branch - Take
locale/manageiq.pot
generated in the previous step and upload it to Transifex to master resource - In ManageIQ core git checkout, switch to the jansa branch
- Upload the language catalogs
locale/${lang}/manageiq.po
from the jansa branch to Transifex, master resource - Once you upload the language catalogs from previous step, Transifex will do all the matching & merging of strings
- In ManageIQ core git checkout, switch to the master branch
- Download the newly merged language catalogs from Transifex master resource and commit them to the git master branch