DEV Community

Bryan Ollendyke
Bryan Ollendyke

Posted on

Reviewing i18n solutions

In deciding to build i18n-manager after fleshing out reasons in our issue queue (the solution I came up with for addressing i18n in our web components) I started where I always do with front end code: See if someone already solved this. After some searching I found four solutions that were in the web components space:

  1. lit-translate
  2. note-list
  3. lit-element-i18n
  4. @lit/localize

There could be full blog posts written about what's going on in each of these so I'll keep it short as to what I liked and didn't like about each, ultimately leading us to create our own. The biggest issue in all of them was either dependencies chains tied to specific project, poor DX, or assumption of building a full application and thus 1 app-{lang}.json data blob that our project will not handle.


This probably came the closest to what we implemented and took ideas from (except for "one for each language you support"). I loved that it used lit and was light weight. In it you called the translate function and registered your translations globally. Then as language was changed (which it ships with a nice demo) it'll toggle.

import { use, translate } from "lit-translate";
import { LitElement, html } from "lit-element";

export class MyApp extends LitElement {

  // Construct the component
  constructor () {
    this.hasLoadedStrings = false;

  // Defer the first update of the component until the strings have been loaded to avoid empty strings being shown
  shouldUpdate (changedProperties) {
    return this.hasLoadedStrings && super.shouldUpdate(changedProperties);

  // Load the initial language and mark that the strings have been loaded.
  async connectedCallback () {

    await use("en");
    this.hasLoadedStrings = true;

  // Render the component
  protected render () {
    return html`

customElements.define("my-app", MyApp);
Enter fullscreen mode Exit fullscreen mode

This example also illustrated how to toggle language using the use callback. This definitely got my mind running and I could see how this would be useful to build an application. A UI could toggle the implementation with use and then all the translate calls would update in all elements implementing.

This had a lot of parallels to MobX and how we use it (a thing tweaking state, all elements visualizing that tweak by opting into it).


This wasn't a translation library though an example I found while looking for something else. It's an interesting way of solving this in a single element though.

I really liked how it figured out initial language (something we used in i18n-manager):

get [documentLang]() {
        return (
            document.body.getAttribute("xml:lang") ||
            document.body.getAttribute("lang") ||
            document.documentElement.getAttribute("xml:lang") || 
            document.documentElement.getAttribute("lang") || 
Enter fullscreen mode Exit fullscreen mode

This used a Proxy object in order to quickly make a map where users define their translation by lang code and then it effectively turns calls to translate into the appropriate text at run time. I ended up borrowing a lot of ideas from this and found it a great one-off i18n implementation.


This was also close to winning out and I had a full prototype locally running with it. The DX of this was fantastic but sadly fell to my requirement for lots of small things. This also ties into i18next which is a popular open source / paid service for managing i18n at scale.

Here's an example and we can see how similar the concept is to others reviewed

import { LitElement, html } from 'lit-element'
import { i18nMixin, translate } from 'lit-element-i18n'

class DemoElement extends i18nMixin(LitElement) {

        this.languageResources = '/assets/locales/{{lng}}/{{ns}}.json'

    render() {
        return html`

            <select @change='${this.changeLanguages}'>
                <option value='en'>EN</option>
                <option value='sv'>SV</option>

    changeLanguages(event) {

customElements.define('demo-element', DemoElement)
Enter fullscreen mode Exit fullscreen mode

Note that translate is a function, sitting inside of a LitElement base class. I also enjoy the idea of the i18nMixin SuperClass which then mixes the needed data binding capability into your element. This also requires you hand this.languageResources a string that's using {{lng}} and {{ns}}, placeholders managed by i18next.

This had the strongest DX of any of the options I reviewed. Very easy to implement and read; however it only supported a single file per translation and thus couldn't be used. I also liked that it's the only one to have a namespace concept; which is seen in the example as the app: portion of the call to translate('app:hi'). This meant that you would have a file for that namespace.


This was recommended by Justin Fagnani, core developer of lit-html / LitElement. It's still experimental but it's a lit-html directive that will handle translation. I liked that this was something potentially planned for the future of lit-html / LitElement, which we base a lot of our projects on.

import {html} from 'lit-html';
import {msg} from '@lit/localize';
render(msg(html`Hello <b>World</b>!`), document.body);
Enter fullscreen mode Exit fullscreen mode

However, note I said we base " a lot of our projects on" and not "all projects". This is an extremely powerful, low level helper utility, but we do not want to require people adopt it in order to participate in HAX'ing the web. Lit is never to be a barrier to adoption and if your team say likes to use Stencil, free of dependencies, we don't want you to HAVE to adopt our msg wrapping implementation which effectively requires lit-html in order to correctly manage the template and translate the string.

So where does this leave us?

Well, I really liked all these options in different ways and took a lot of inspiration from them. Now, let's start looking at a new element we're using to manage our internationalization efforts called @lrnwebcomponents/i18n-manager

Discussion (0)