Internationalisation ==================== Internationalisation (also referred to as i19n) is the process of disconnecting the language used in the interface from the interface itself, moving it into a series of configuration files. Setup ----- While we could build our own internationalisation system from scratch, that would be a waste of time, as there are already good solutions out there for ember. The one we will use is "ember-intl": .. code-block:: console $ yarn ember install ember-intl The next step is to configure the ember build process so that it reminds us of all string values that are just being output by the templates and are not being output via the internationalisation system. To do that update the file ``.template-lintrc.js`` so that it looks like this: .. code-block:: js 'use strict'; module.exports = { extends: 'recommended', rules: { 'no-bare-strings': true } }; Translating Template Text ------------------------- Installing the translation package will automatically create a directory ``translations`` and into that place a default translation file ``en-us.yaml``. Before we can start creating the translations, we need to to set-up the initial language. For that, we need to create ``app/routes/application.js`` either by hand or by generating it via the CLI. However, should you choose the latter way, do not overwrite the handlebars file. Update this file to: .. code-block:: js import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; export default class ApplicationRoute extends Route { @service intl; beforeModel() { this.intl.setLocale(['en-us']); } } We will now use the ``yaml``-file to start translating our application. Open up the ``app/components/nav-bar.hbs`` file and update the two menus so that they look like this: .. code-block:: html+handlebars .. code-block:: html+handlebars As you can see, we have replaced the actual text in the menu with the ``{{t}}`` helper. In the minimal form the ``{{t}}`` helper takes a single parameter, which is the unique identifier of the translation text. At the moment we have not provided a translation text, so in the browser you will see error messages. To fix these, update the ``translations/en-us.yaml`` file to look like this: .. code-block:: yaml application: athena_study_portal: Athena - Study Portal my_modules: My Modules my_exams: My Exams register: Register logout: Logout `YAML`_ (YAML Ain't Markup Language) is a simple yet powerful language for writing configuration files. At the basic level that we will be using here, our configuration files consist only of nested key-value pairs. At the top level we have a single key "application" and then within that we have five nested keys for specific texts used in the application template. When the internationalisation service loads the translation file, each nesting in the translation file is transformed into a ".". So the nesting structure .. _`YAML`: http://yaml.org/ .. code-block:: yaml application: athena_study_portal: Athena - Study Portal is translated into the translation key ``"application.athena_study_portal"``, which is what we have used in the template. Now that we have all the keys in the translation file, you will see that our interface is back to the way we expect it to look. Adding a Second Language ------------------------ Now translating like this to a single file is relatively pointless, so the next step is to add a German translation to our application. To do this, we will first create a component class for our ``NavBar`` component by running .. code-block:: console $ yarn ember generate component-class nav-bar and then update the second menu in the ``app/components/nav-bar.hbs`` to have two entries to allow us to switch between the two languages: .. code-block:: html+handlebars As you can see, for the default language (English), we simply call the ``setLocale`` action without any parameters, while for the German language we call it with the full language name ``"de-de"``. Next, we need to update ``app/components/nav-bar.js`` to handle this action to this: .. code-block:: js import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; export default class NavBarComponent extends Component { @service intl; @action setLocale(primaryLocale) { let locale = ['en-us']; if (primaryLocale) { locale.splice(0, 0, primaryLocale); } this.intl.setLocale(locale); } } Two things are important here. First, the internationalisation is provided as another service, so we also load it into the controller as such. This is not necessary if you just want to translate via the ``{{t}}`` helper, but as we want to change the translation locale, we need to explicitly inject the service. Second, in the ``setLocale`` action, we define the new ``locale`` as a list of languages in order of descending preference and initialise it with the value ``['en-us']``. This is the default and thus also the fall-back language. Next, if a language was provided via the ``primaryLocale`` parameter, then we use the ``splice`` function to insert this into the front of the ``locale`` list. Finally, we use the ``setLocale`` function to inform the internationalisation system that a new locale is now to be used when translating text. The reason for this structure is that writing the application and the translations are generally separate jobs. The application developer will generally provide the translation for the default language, while separate translator(s) will provide the other language translations. So that the user does not see error messages everywhere when a translation has not yet been completed, we set a locale that includes both the language the user wants to see and the default language. When the translation system then encounters text that has not yet been translated, the translation system can fall back on the default language, which will contain a translation. Later, when the translator provides the translation, this will then be used instead. You can try this out in the application by switching to the "German" translation. You will see that this has no effect, as we have not yet provided a German translation. To do so, create a new file ``translations/de-de.yaml`` and copy the contents of the ``translations/en-us.yaml`` into that. Then translate the actual values into German: .. code-block:: yaml application: athena_study_portal: Athena - Studienportal my_modules: Meine Module my_exams: Meine Prüfungen register: Registrieren logout: Ausloggen You can now test switching between languages and you will see that the interface automatically updates. The only limitation of the interface at the moment is that there is no indication to the user, which language is currently selected. To implement that, update the ``app/components/nav-bar.hbs`` language menu to the following: .. code-block:: html+handlebars As you can see, we use to ``{{if}}`` blocks to distinguish whether each of the languages is active or not. To get the active language, we use ``intl.locale.[0]``. The first part ``intl.locale`` access the internationalisation service and within that the list of languages set for the current locale. As this is a list, we then use ``.[0]`` to retrieve the first element of the list, which is the primary locale. Try it out to see that you now get visual feedback on the language selection. Structuring Translations ------------------------ The important thing for maintaining the long-term maintainability of the translation structure is to have a structure that ensures minimal duplication in the translation files, while retaining understandable and sensible translation key names. As an example, update the ``app/components/modules/modules-navbar.hbs`` so that the menu there is also translated: .. code-block:: html+handlebars

{{t 'application.my_modules'}}

As you can see, we have added three translation keys "generic.current_semester", "generic.last_semester", "modules.enroll", and "modules.create". This means that we have added two top-level keys "generic" and "modules". The logic behind this split is that the translation text for the current and last semester are likely to be used across the application and are not specific to the modules page. Thus we place those in the "generic" section. On the other hand the enroll and create new functions are completely module-specific, thus should be structured in a module-specific section of the translations. Update the two translation files ``translations/en-us.yaml`` and ``translations/de-de.yaml``: .. code-block:: yaml generic: current_semester: Current Semester last_semester: Last Semester application: athena_study_portal: Athena - Study Portal my_modules: My Modules my_exams: My Exams register: Register logout: Logout modules: enroll: Enroll in Module create: Create a new Module .. code-block:: yaml generic: current_semester: Aktuelles Semester last_semester: Letztes Semester application: athena_study_portal: Athena - Studienportal my_modules: Meine Module my_exams: Meine Prüfungen register: Registrieren logout: Ausloggen modules: enroll: In Modul Einschreiben create: Neues Modul Erstellen Now, we have a working application available in two different languages and adding further languages is easy. However, if you switch languages a few times, you will also see the difficulty with internationalisation and that is that for all of the translated texts, the German is longer than the English text. In other languages this can be even worse. When designing an internationalisable interface, it is thus important to take this into account.