How to achieve manageable i18n in your Angular app

David Palita
WhozApp
Published in
3 min readMar 25, 2019

At Whoz we have a sizeable Angular frontend, with well over 2,000 translated strings defined via the excellent ngx-translate module.

TL;DR: structure your keys by Angular component except for the common vocabulary and the domain model, use java properties instead of json and simplify your job with the ngx-translate-messageformat-compiler:

Before (not cool) and after (cool), read on to find out why!

At first, it is easy to structure your json translation file, probably as many examples show by page and section. But then you have components used in many pages, common strings you want to keep DRY, refactorings that are not reflected in the json structure because the impact is overwhelming…

As we are a startup, you can imagine we tried, we failed, we tried again, and even changed directions. Along with these evolutions, the translation corpus loosely followed our path. We eventually found ourselves with around 40% unused translations, some missing translations… and no way to tell which.

The mess we were in is due to 2 factors :

  • we lacked clear and strict guidelines as to how translation keys should be chosen
  • the default ngx-translate recommendation uses a json structure to declare them and a “pointed string path” to use them… making it quite hard to navigate from usage to definition and the other way around

We tackled both issues and made our strings great again. Here is how:

Define clear and strict guidelines for your translation keys

Rules are cool if they are easy to follow (unambiguous and not too constraining) because they may be trusted to handle most use cases without you having to rethink about all of them every time, thus saving brain power for better tasks.

  • never use dynamic keys like{{'some.' + unfindable + '.dynamicKey' | translate }} ) as they are impossible to find, refactor, etc.
  • every i18n key start with “i18n”. It is not a big hassle and allows for searching “i18n.*” in your project to find all translated strings
  • every i18n key continues with the component class name which uses it: “i18n.MyComponent.”. This instantly identifies the intended usage of the string and is very straightforward as to what path a given string should have
  • the keys are otherwise flat and simple: “i18n.MyComponent.myKey”. By not trying to incorporate the graphical usage into the key (either by appending “.button” or “.label” or by nesting by prepending “.topSection” or “.form”) you keep your keys simple, logical and resilient to graphical refactoring.

There are 2 exceptions to the “component class name” rule :

  • Extremely common strings without business domain semantics are in a “i18n.Common” path. This DRYes up things like “Save”, “Cancel”, “This field is required”, …
  • Business domain model fields are in their class name path “i18n.MyModel.myField”. This DRYes up all the usages of these strings you usually encounter as labels in the related domain object form, as column titles in a table listing the related objects, in confirmation messages, etc. This also eliminates the problem where you gradually diverge towards different wordings for the same concept.

Use a java “.properties” file format instead of json

There are 2 great advantages in doing so :

  • you have a 1–1 mapping between definition and usage of your translation key. It is then trivial to find all usages, do a refactor, jump to the definition from the template, …
  • it has excellent support in the IntelliJ product family where you can edit them as resource bundles. You can edit all languages of a key at once, find missing languages at a glance, etc. And you can still display them nested as if it was json (and sorted if you prefer).
IntelliJ groups and sorts keys in an i18n resource bundle

You still load them as json with the json and enhanced-properties loaders as follow (or any equivalent runtime solution):

require('!json-loader!enhanced-properties-loader!./translations.fr.properties')

Bonus: use ngx-translate-messageformat-compiler

You can read about the ICU message format and how it can solve your gender and plural management problems. It will drastically reduce the number of keys you need and greatly simplify their handling in the templates.

This also greatly helps with the rule “no dynamic i18n key” because we can handle our enums like so:

Use message format to handle enums

--

--