Custom element scoped translations

Component oriented development definitely is no novelty in these days. A lot of frameworks like Aurelia, React, Angular and all the others do support it nicely. One of the ideas of that approach is to make components self-aware about their intention and fully encapsulated. Data is sent into components via inputs, props, bindables — similar concepts just different names from the frameworks out there — and that is rendered using some component internal UI logic.

That's all fine but another aspect are auxiliary information. A component representing a page in a SPA might store and configure the route by itself. Contrary to what Angular does with its router, but fully ok with other frameworks. One benefit of that is simpler partial/lazy app loading. Another example is CSS, which is directly tied to a specific component. This approach has great momentum in the React community and with the advent of CSS Modules might be a good universal fit.

We call those things scoped resources/settings as they are directly related to a component. Now, what if we could do the same with translation resources?

Separating translations by namespaces

If you haven't yet, this is the perfect time to take a look at Aurelia's I18N Plugin and the docs section on how to get started. It wraps the fantastic i18next library which offers amongst others a great way to structure your translations by using namespaces. With this, you can not only split translations into various files but also spaces.

So when you start, following the tutorial section in the hub, you'll end up with a file called translation.json, created for each respective language your app supports. This file resembles the default namespace translation. Your full resources structure might look as follows, given you support the locales en-US and de-DE:

{
  "en-US": {
    "translation": {
      "title": "Hello from Aurelia"
    }
  },
  "de-DE": {
    "translation": {
      "title": "Hallo von Aurelia"
    }
  }
}

As you see the structure contains the locale on the top level, followed by the default namespace. What we would like to achieve now is to give each custom element it's custom namespace. So for a custom element named MyElement that should end up being something like follows:

{
  "en-US": {
    "translation": {
      "title": "Hello from Aurelia"
    },
    "MyElement: {
     ... // translation keys with en-US locale
    }
  },
  "de-DE": {
    "translation": {
      "title": "Hallo von Aurelia"
    },
    "MyElement: {
     ... // translation keys with de-DE locale
    }
  }
}

Scoped translation resources

To keep the translation parts now inline with the custom element we'll be using a decorator called scopedI18N like this in our elements ViewModel my-element.js:

import {scopedI18N} from './scoped-i18n';

@scopedI18N({
  de-DE: {
    greeting: 'Hallo!',
    info: 'Du siehst hier komponentengebundene Übersetzungen'
  },
  en-US: {
    greeting: 'Hi there!',
    info: 'You\'re seeing scoped component translations'
  }
})
export class MyElement {  
  constructor() {
  }
}

The decorator accepts an object, which defines locales in the first and your translation keys in the second level. After that, you can use those keys in the custom elements view my-element.html by prefixing the key with the custom elements namespace, by default the elements class name.

<template>  
  <h1>${ 'MyElement:greeting' & t }</h1>

  <span t="MyElement:info">...</span>
</template>  

Building the scopedI18N decorator

Last but not least it's time to build the decorator. We'll do so creating a file scoped-i18n.js:

import { Container } from 'aurelia-framework';  
import { I18N } from 'aurelia-i18n';

export function scopedI18N(translations) {  
  return function(target) {
    const i18nService = Container.instance.get(I18N);

    Object.keys(translations).forEach( (lng) => {
      i18nService.i18next.addResources(lng, target.name, translations[lng]);
    });
  };
}

Let's go through the code step by step.

First, we are importing Aurelia's IOC Container alongside the I18N service from the plugin.
Next, we have to create and export the decorator, which essentially is a higher-order function named scopedI18N, accepting an attribute called translations. This shell returns a function accepting the class target which we're applying our decorator to. In here we now can use the IOC container to get an instance of the i18n service.
Last but not least we'll iterate over the provided translation object. Remember the first level keys are the locales we're using.
We're going to use the i18next reference, provided by the service in order to add resources which we do for each locale, using the target's name as namespace and applying the contents of the respective locale.

Conclusion

And that's it. With these few lines, we've created a nice and concise way to add scoped translations to any component. With this, you can now not only have your CSS and View but also your translations scoped to your custom element.

photo credit: Unsplash: Binoculars via Pixabay (license)