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”:
$ 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:
'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:
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:
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:
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
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
$ 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:
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:
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:
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:
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:
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
:
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
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.