ember-intl is the most common solution for managing translations in Ember apps. It provides a number of useful tools to format messages in both code and template files, and even includes test-helpers.
One downside of the default configuration of ember-intl is that it adopts a key-based translation system, where instead of writing the strings that your end user will see in your component templates, you write a dot-separated key that represents a nested data structure of your translation files. This can be quite a challenging system for teams of any scale to manage:
this.intl.t('you.must.maintain.this');
Other tools, like FormatJS, allow you to invert the process of translation by auto-generating keys from translation strings in your app, and extract those translations into a file formatted correctly:
// You can write this in your code, it will end up in your translation file.
intl.formatMessage({
defaultMessage: 'Hello world',
});
ember-formatjs
integrates ember-intl
and formatjs
to make use of the inline translation strings recommended by FormatJs while sticking to ember-intl
to manage translations at runtime.
- Ember classic >= 3.28
ember install ember-intl ember-formatjs
Configure your locale following ember-intl
starter guide.
In a template, use the {{format-message}}
helper with the message the end user sees:
In js
and ts
files, inject ember-intl
service to use translations, but rely on the formatMessage
method to define the message descriptor:
import { service } from '@ember/service';
export default class MyEmberController extends Controller {
@service intl;
@action
onGreeting() {
this.modal.show(
this.intl.formatMessage({
defaultMessage: 'Hello!',
}),
);
}
}
Since ember-formatjs
relies on both ember-intl
and format-js
, you need to acquire a bit of knowledge about both of these tools to figure out what's going on and how to setup the workflow you prefer. In this section, let's put an example of what FormatJs docs explain in "The workflow" section.
In your application whole lifecycle, from written code to runtime, your translation files, at some point, will exist in 2 different versions: the "runtime" format, and the translation management system (TMS) format (Crowdin, Lokalise, Smartling...)
The runtime format is the one expected by the lib you use to manage translations at runtime:
{
"wafoOY": "I am formatted for ember-intl"
}
The TMS format is the one you need to communicate with the TMS you use:
{
"wafoOY": {
"message": "I am formatted for Crowdin"
},
"wafoOW": {
"translation": "I am formatted for Lokalise"
}
}
FormatJs finds all the formatted messages in your codebase and extract them into a translation file like en-us.json
; and since it knows that you probably work with a TMS, it offers you ways to format the json
for the TMS you use, using optional formatters. If your TMS is not supported, you can write your own formatter.
When using ember-formatjs
, you can install @formatjs/cli
and include the extract command from @formatjs/cli
in your workflow. For instance:
"extract": "npx formatjs extract './app/**/*.(hbs|js)' > ./locale/en-us.json"
The result of this command creates the "source of truth" file that you upload to the TMS. Then, your translators will complete the translations (fr-fr, de-de, en-gb...), and you will include the download somewhere in your workflow. The files you download from the TMS are in the TMS format, and therefore are not usable at runtime, ember-intl
doesn't understand them.
To put them back to a "runtime" format that ember-intl
understands, you can use the compile
command from @formatjs/cli
:
"compile": "npx formatjs compile './locale/en-us.json' --out-file './translations/en-us.json'"
And this how you close the loop: you write English messages in the code, FormatJs extract them in a locale/en-us.json
that you upload to the TMS, then you download the supported language files from the TMS, compile them and store them in the translations/
folder that ember-intl
can use to get your translations working at runtime.
The section above gives you the key commands to get things working but you might be a bit frustrated that it doesn't say more exactly where and when to run them. This is because many different workflows are possible. For instance, you can run these commands manually or get the CI execute them; you could decide to stage only the TMS version of the translation files and do the compile at build time; or you could produce directly the compiled version of the source file and stage it, then rely on some custom tools to communicate with a TMS...
FormatJS documents a process for extraction and compilation with a single script for applications that might not want to integrate a TMS into your process. This way you would directly extract your translations from your codebase into your default translation file and then you can manually maintain your alternative translation files in your repo.
Here is the example script that you can use to extract and output your translation in one command:
formatjs extract './app/**/*.(hbs|js)' --out-file temp.json && formatjs compile 'temp.json' --out-file translations/en-us.json && rm temp.json
When using FormatJs, ids are generated by transforming the string message with an interpolation pattern. It means that as long as the message doesn't change, the id doesn't change either. Whatever you use FormatJs alone or @formatjs/cli
and ember-formatjs
together, the id generation takes place twice:
-
Extraction: The strings messages detected in the code are extracted by FormatJs
extract
command in the translations source file (e.g.locale/en-us.json
). So in the source file, you have an id is generated to serve as message keys. -
Code transpilation: during the build,
ember-formatjs
translates your messages' syntax{{format-message 'Hello'}}
into the classicember-intl
syntax{{t 'OpKKos'}}
, which is key-based, and therefore an id is generated for the key. This translation layer allowsember-intl
to behave as usual at runtime.
To sum it up, the interpolation pattern you use for extraction and transpilation should be exactly the same, else you'll end up with a mismatch between the keys in your json
and the keys written in your output code, leading to missing translations issues. The default interpolation pattern is [sha512:contenthash:base64:6]
.
A custom idInterpolationPattern
can be set in the ember-cli-build.js
if needed.
let app = new EmberApp(defaults, {
'ember-formatjs': {
idInterpolationPattern: '[sha512:contenthash:base64:6]', //this interpolation pattern is default
},
});