Guidelines for Internationalization

Table of Contents

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) – translates msgid and returns the translated string
  • N_(msgid) – dynamic translation: this will put msgid 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 translated msgid or its translated plural form (msgid_plural) depending on n, a number determining the count (i.e. number above 1 means plural form)
  • s_(msgid, seperator = "|") – translates msgid, but if there are no localized text, it returns a last part of msgid separated by separator (| by default)
  • ns_(msgid, msgid_plural, n, seperator = "|") – similar to the n_(), but if there is no localized text, it returns a last part of msgid separated by separator.

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, 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 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 by gulp 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 by gulp 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 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)

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 Zanata and navigate to 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?
    • 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.

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 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.

Zanata

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.

ManageIQ (the main project)

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

ManageIQ Self Service UI

  • 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

ManageIQ Amazon Provider

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

Internationalization in rails engines (plugins)

  • 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 https://github.com/ManageIQ/manageiq-providers-amazon/pull/28 as an example.