Internationalization for GitLab

Introduced in GitLab 9.2.

For working with internationalization (i18n), GNU gettext is used given it’s the most used tool for this task and there are many applications that help us work with it.

note
All rake commands described on this page must be run on a GitLab instance. This instance is usually the GitLab Development Kit (GDK).

Setting up the GitLab Development Kit (GDK)

To work on the GitLab Community Edition project, you must download and configure it through the GDK.

After you have the GitLab project ready, you can start working on the translation.

Tools

The following tools are used:

  • gettext_i18n_rails: this gem allows us to translate content from models, views, and controllers. It also gives us access to the following Rake tasks:

    • rake gettext:find: parses almost all the files from the Rails application looking for content marked for translation. It then updates the PO files with this content.
    • rake gettext:pack: processes the PO files and generates the binary MO files that the application uses.
  • gettext_i18n_rails_js: this gem makes the translations available in JavaScript. It provides the following Rake task:

    • rake gettext:po_to_json: reads the contents of the PO files and generates JSON files that contain all the available translations.
  • PO editor: there are multiple applications that can help us work with PO files. A good option is Poedit, which is available for macOS, GNU/Linux, and Windows.

Preparing a page for translation

There are four file types:

  • Ruby files: models and controllers.
  • HAML files: view files.
  • ERB files: used for email templates.
  • JavaScript files: we mostly work with Vue templates.

Ruby files

If there is a method or variable that works with a raw string, for instance:

def hello
  "Hello world!"
end

Or:

hello = "Hello world!"

You can mark that content for translation with:

def hello
  _("Hello world!")
end

Or:

hello = _("Hello world!")

Be careful when translating strings at the class or module level since these are only evaluated once at class load time. For example:

validates :group_id, uniqueness: { scope: [:project_id], message: _("already shared with this group") }

This is translated when the class loads and results in the error message always being in the default locale. Active Record’s :message option accepts a Proc, so do this instead:

validates :group_id, uniqueness: { scope: [:project_id], message: -> (object, data) { _("already shared with this group") } }

Messages in the API (lib/api/ or app/graphql) do not need to be externalized.

HAML files

Given the following content in HAML:

%h1 Hello world!

You can mark that content for translation with:

%h1= _("Hello world!")

ERB files

Given the following content in ERB:

<h1>Hello world!</h1>

You can mark that content for translation with:

<h1><%= _("Hello world!") %></h1>

JavaScript files

The ~/locale module exports the following key functions for externalization:

  • __() Mark content for translation (double underscore parenthesis).
  • s__() Mark namespaced content for translation (s double underscore parenthesis).
  • n__() Mark pluralized content for translation (n double underscore parenthesis).
import { __, s__, n__ } from '~/locale';

const defaultErrorMessage = s__('Branches|Create branch failed.');
const label = __('Subscribe');
const message =  n__('Apple', 'Apples', 3)

To test JavaScript translations, learn about manually testing translations from the UI.

Vue files

In Vue files, we make the following functions available to Vue templates using the translate mixin:

  • __()
  • s__()
  • n__()
  • sprintf

This means you can externalize strings in Vue templates without having to import these functions from the ~/locale file:

<template>
  <h1>{{ s__('Branches|Create a new branch') }}</h1>
  <gl-button>{{ __('Create branch') }}</gl-button>
</template>

If you need to translate strings in the Vue component’s JavaScript, you can import the necessary externalization function from the ~/locale file as described in the JavaScript files section.

To test Vue translations, learn about manually testing translations from the UI.

Test files

Test expectations against externalized contents should not be hard coded, because we may need to run the tests with non-default locale, and tests with hard coded contents will fail.

This means any expectations against externalized contents should call the same externalizing method to match the translation.

Bad:

click_button 'Submit review'

expect(rendered).to have_content('Thank you for your feedback!')

Good:

click_button _('Submit review')

expect(rendered).to have_content(_('Thank you for your feedback!'))

This includes JavaScript tests:

Bad:

expect(findUpdateIgnoreStatusButton().text()).toBe('Ignore');

Good:

expect(findUpdateIgnoreStatusButton().text()).toBe(__('Ignore'));

Recommendations

If strings are reused throughout a component, it can be useful to define these strings as variables. We recommend defining an i18n property on the component’s $options object. If there is a mixture of many-use and single-use strings in the component, consider using this approach to create a local Single Source of Truth for externalized strings.

<script>
  export default {
    i18n: {
      buttonLabel: s__('Plan|Button Label')
    }
  },
</script>

<template>
  <gl-button :aria-label="$options.i18n.buttonLabel">
    {{ $options.i18n.buttonLabel }}
  </gl-button>
</template>

Also consider defining these strings in a constants.js file, especially if they need to be shared across different modules.

  javascripts
  
  └───alert_settings
        constants.js
     └───components
            alert_settings_form.vue


  // constants.js

  import { s__ } from '~/locale';

  /* Integration constants */

  export const MSG_ALERT_SETTINGS_FORM_ERROR = __('Failed to save alert settings.')


  // alert_settings_form.vue

  import {
    MSG_ALERT_SETTINGS_FORM_ERROR,
  } from '../constants';

  <script>
    export default {
      MSG_ALERT_SETTINGS_FROM_ERROR,
    }
  </script>

  <template>
    <gl-alert v-if="showAlert">
      {{ $options.MSG_ALERT_SETTINGS_FORM_ERROR }}
    </gl-alert>
  </template>

Using either constants or $options.i18n allows us to reference messages directly in specs:

import { MSG_ALERT_SETTINGS_FORM_ERROR } from 'path/to/constants.js';

// okay
expect(wrapper.text()).toEqual('this test will fail just from button text changing!');

// better
expect(wrapper.text()).toEqual(MyComponent.i18n.buttonLabel);
// also better
expect(wrapper.text()).toEqual(MSG_ALERT_SETTINGS_FORM_ERROR);

Dynamic translations

Sometimes there are dynamic translations that the parser can’t find when running bin/rake gettext:find. For these scenarios you can use the N_ method. There’s also an alternative method to translate messages from validation errors.

Working with special content

Interpolation

Placeholders in translated text should match the respective source file’s code style. For example use %{created_at} in Ruby but %{createdAt} in JavaScript. Make sure to avoid splitting sentences when adding links.

  • In Ruby/HAML:

    _("Hello %{name}") % { name: 'Joe' } => 'Hello Joe'
    
  • In Vue:

    Use the GlSprintf compon