ManageIQ uses gettext for internationalization. In particular, the project uses the following gems to make internationalization work:
ManageIQ supports internationalization of strings in ruby and haml sources as well as JavaScript.
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
.
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.
Purely Angular applications, such as Self-Service UI, 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
).
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.
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 gulp 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 bygulp 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 bygulp 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");
}
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 Tennants'))
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)
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")
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«
To contribute with translation to ManageIQ, create an account at Zanata and navigate to ManageIQ project page
-
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?local_name
meens 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.
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 Zanata).
- 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.
We use Zanata for online translations. We maintain several zanata projects for the ManageIQ project:
To be able to use zanata from command line, make sure you have zanata-cli
installed and configured (documentation)
To be able to manipulate zanata catalogs, you need to be maintainer of the project in zanata.
Steps for updating translations:
- Update gettext catalogs in ManageIQ git repository. This is done to make sure the gettext catalog contains up to date (i.e. current) strings for translators. To update the catalog, run the following rake task in the root of ManageIQ git checkout:
$ bundle exec rake locale:update
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
Now, commit and push changed files into git (branch, pull request, etc.). Make sure no other files than the following list is commited:
config/model_attributes.rb
config/dictionary_strings.rb
config/yaml_strings.rb
config/locales/manageiq.pot
config/locales/*/*.po
- Upload the catalog created in previous step to zanata:
$ cd config/locales
$ zanata-cli push --push-type source
Now translators in zanata will have the latest stuff to translate available.
- Once the translators finish translations of a particular language, pull the translated catalog from zanata back to ManageIQ repository.
Run the following in ManageIQ git checkout:
$ cd config/locales
$ zanata-cli pull --pull-type trans # add --locales ... if you wish to download only specific locales
$ bundle exec rake gettext: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 no other files than the following list is commited:
config/locales/*/*.po
app/assets/javascripts/locale/*/*.js
config/human_locale_names.yaml # optionally, if you added locales
- Update the gettext catalogs:
$ cd client
$ gulp gettext-extract
- Upload the catalogs to zanata:
$ zanata-cli push --push-type source
- Once the translations are complete, download the translated catalogs:
$ zanata-cli pull --pull-type trans --locales ...
- Convert the translated .po catalogs into .json files:
$ gulp gettext-compile
- Update locale names:
$ gulp available-languages
- Commit and push the new content into git:
$ git commit client/gettext/po client/gettext/json
$ git push
This procedure would apply to all rails engines hooked into ManageIQ in general.
- Update the gettext catalog. With the Amazon provider engine (plugin) hooked into your ManageIQ instance, run the following from the ManageIQ git checkout:
$ rake locale:plugin:find[ManageIQ::Providers::Amazon]
- Upload the catalog to zanata:
$ zanata-cli push --push-type source
- Once the translations are complete, download the translated catalogs:
$ zanata-cli pull --pull-type trans --locales ...
- Commit and push the new catalogs into git:
$ git commit locale/
$ git push
- Create an initializer in your engine / plugin with a content similar to:
$ cat config/initializers/gettext.rb
Vmdb::Gettext::Domains.add_domain('ManageIQ_Providers_Amazon',
ManageIQ::Providers::Amazon::Engine.root.join('locale').to_s,
:po)
- Initial extraction of gettext strings found in your engine / plugin:
$ cd /path/to/your/plugin/root
$ mkdir ./locale
$ for locale in en, ...; do mkdir ./locale/$locale; done
$ cd /path/to/your/manageiq/root
$ rake locale:plugin:find[YourPlugin]
Refer to ManageIQ/manageiq-providers-amazon/pull/28 as an example.