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:
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.
lit-translate
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 () {
super();
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 () {
super.connectedCallback();
await use("en");
this.hasLoadedStrings = true;
}
// Render the component
protected render () {
return html`
<p>${translate("title")}</p>
`;
}
}
customElements.define("my-app", MyApp);
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).
note-list
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") ||
FALLBACK_LANG
);
}
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.
lit-element-i18n
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) {
constructor(){
super();
this.languageResources = '/assets/locales/{{lng}}/{{ns}}.json'
}
render() {
return html`
<h1>${translate('app:hi')}</h1>
<select @change='${this.changeLanguages}'>
<option value='en'>EN</option>
<option value='sv'>SV</option>
</select>
`
}
changeLanguages(event) {
this.changeLanguage(event.target.value)
}
}
customElements.define('demo-element', DemoElement)
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.
@lit/localize
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);
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
Top comments (0)