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 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 by yarn 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 by yarn 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
$ 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:

  1. Run bundle exec rake locale:update_all in ManageIQ core on master branch
  2. Take locale/manageiq.pot generated in the previous step and upload it to Transifex to master resource
  3. In ManageIQ core git checkout, switch to the jansa branch
  4. Upload the language catalogs locale/${lang}/manageiq.po from the jansa branch to Transifex, master resource
  5. Once you upload the language catalogs from previous step, Transifex will do all the matching & merging of strings
  6. In ManageIQ core git checkout, switch to the master branch
  7. Download the newly merged language catalogs from Transifex master resource and commit them to the git master branch