i18n-manager web component
Tags: i18n, haxtheweb, webcomponents, webdev
Published: 2021-03-03 18:20:32

[i18n-manager](https://github.com/elmsln/lrnwebcomponents/tree/master/elements/i18n-manager) is an element and helpers classes produced to help us manage our i18n needs in web components.

Recapping requirements:
- Must work without requiring dependencies to adopt
- Must have good / simple DX
- Must work with multiple translation files

Goal is to translate ALL of our elements but ultimately supply full internationalization support for the HAX editor, a complex Mobx powered series of web components to transform web editing.

# What integration looks like
Here's a simple example of an integration using our [@lrnwebcomponents/self-check](https://www.npmjs.com/package/@lrnwebcomponents/self-check) web component. (code simplified to highlight integration only)

```js
import { LitElement, html, css, svg } from "lit-element/lit-element.js";
import { I18NMixin } from "@lrnwebcomponents/i18n-manager/lib/I18NMixin.js";

class SelfCheck extends I18NMixin(LitElement) {
  constructor() {
    super();
    this.t = {
      reveal: "Reveal Answer",
      close: "Close",
      more: "More information",
    };
    this.registerTranslation({
      context: this,
      basePath: import.meta.url,
      locales: ["en-UK", "ja"],
    });
  }
  static get styles() {
    return [
      ...super.styles,
      css`
        :host {
          display: block;
          margin: 15px 0;
        }
      `,
    ];
  }
  render() {
    return html`
    <div class="card">
      <simple-icon-button
        controls="answer_wrap"
        aria-label="${this.t.reveal}"
        id="checkBtn"
        class="check-btn"
        icon="icons:check-circle"
        ?dark="${this.dark}"
        @click="${this.openAnswer}"
      ></simple-icon-button>
      <simple-tooltip aria-hidden="true" for="checkBtn" position="left">
        ${this.t.reveal}
      </simple-tooltip>
      <user-action track="click" every
        ><a href="${this.link}" target="_blank"
          >${this.t.more}...</a
        ></user-action
      >
    </div>`;
  }
}
window.customElements.define(SelfCheck.tag, SelfCheck);
export { SelfCheck };
```

This has 4 key parts:
- `I18NMixin` is applied to LitElement base class.
- `this.t = {}` has keys which are then supplying strings.
- `this.registerTranslation` supplies the context, a reference to the current file location (the `import.meta.url`) portion and then an `Array` of `locales` that it supports
- the `render` function then leverages `this.t.reveal` which will print the text in its place, defaulting to `en`

From the code above:
```js
this.registerTranslation({
      context: this,
      basePath: import.meta.url,
      locales: ["en-UK", "ja"],
    });
```
We can see that the `self-check` element supports English, British English (so dialect within a language), and Japanese. The locales + basePath imply that there are JSON files located in a pattern of `{pathToThisElement}/locales/{tagName}.{lang}.json` and then data found there is injected back into this element when language changes. This causes the `this.t` object to be updated, which in turn causes the element to reflect the update.

[self-check.ja.json](https://github.com/elmsln/lrnwebcomponents/blob/master/elements/self-check/locales/self-check.ja.json)

```json
{
  "reveal": "答えを明らかにする",
  "close": "閉じる",
  "more": "詳細"
}
```

This integration has LitElement in mind and while the mixin is vanilla, it's making some assumptions about LitElement obviously via the `this.t` as `t` is supplies as an `Object` in the properties `getter`. However, this can work with 0 dependencies as the main thing happening in this Mixin is simply sending an event!

## zero dependency integration
Here's all you need to work with `i18n-manager`:
```js
constructor() {
  super();
  this.t = {
    whatever: "Whatever"
  };
  window.dispatchEvent(
      new CustomEvent("i18n-manager-register-element", {
        detail: {
          context: this,
          namespace: "simple-login",
          localesPath:
            new URL('../locales', import.meta.url).href,
          updateCallback: "render",
          locales: ["es"],
        },
      })
    );
}
```
### uhh... how'd you do that!?
This supplies a context `this`, namespace (name of the element), a resolved `localesPath` based on loaded file location on the front end, an updateCallback (saying to run render on variable change), and then a locales `Array`.

Then it all magically works because, Magic.

# No really, how we made that work
This works because we've adopted a Singleton pattern; meaning that we have a single web component that sits in the DOM and is in charge of a single concept of state management. In this case, `i18n-manager` is in charge of JUST translation. Who cares about translation, what translations are supplied, what the language currently is, and who to notify when language changes.

The next post will go a bit into how `i18n-manager`'s internals are constructed and lead into a video stepping through the whole process described in these posts.