The inimitable Nikki Massaro Kaufman has been working on the upcoming Red Hat audio player web component. It's an impressive piece of work that packs a tonne of features into such a small package. We got an order to implement client-side translations, so Nikki opted for an approach that lazy-loads key-value pairs into the app.
Initially, this was set up as a POJO, but that had a couple of drawbacks
- We needed to use bracket access, or wrap that up in a dedicated private
instance method
#translate(key: string) { return this.#microcopy[key] ?? key; }
- Whenever modifying the table (e.g. when lazy loading) we had to rememeber to
sprinkle in
this.requestUpdate()
to make sure to rerender
Squinting, I started to see the outline of reactive controller with one eye and a Map with the other. Problem is maps don't have default getters.
Turns out the modern web platform is a joy to work with:
import type { ReactiveController, ReactiveControllerHost } from 'lit';
export class MicrocopyController extends Map<string, string> implements ReactiveController {
hostConnected?(): void
}
For ergonomics, let's accept a POJO:
constructor(private host: ReactiveControllerHost, obj: Record<string, string>) {
super(Object.entries(obj));
}
Then we'll implement our default getter:
get(key: string) {
return super.get(key) ?? key;
}
Since the strings are lazy loaded, and the lang can change as a result of user input, we can round it out with some built-in reactivity:
set(key: string, value: string) {
super.set(key, value);
this.host.requestUpdate();
return this;
}
clear() {
super.clear();
this.host.requestUpdate();
}
delete(key: string) {
const r = super.delete(key);
this.host.requestUpdate();
return r;
}
For the lulz, let's add a join
method that lets us fold in new definitions:
join(obj: Record<string, string>) {
for (const [key, value] of Object.entries(obj)) {
this.set(key, value);
}
this.host.requestUpdate();
return this;
}
et voila, we're done!
Use it like this:
import { html, css, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { MicrocopyController } from './microcopy-controller.js';
const LANGS = {
'en-us': {
hello: 'Hello'
},
'he-il': {
hello: 'שלום',
}
};
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
static styles = css`p { color: blue }`;
#i18n = new MicrocopyController(this, LANGS['en-us']);
@property() name = 'Somebody';
@property() lang = 'en-us';
willUpdate(changed) {
if (changed.has('lang'))
this.#i18n.join(LANGS[this.lang]);
}
render() {
return html`
<form @change=${e => this.lang = e.target.elements.lang.value}>
<label>English<input type="radio" name="lang" value="en-us"></label>
<label>Hebrew<input type="radio" name="lang" value="he-il"></label>
</form>
<p>${this.#i18n.get('hello')}, ${this.name}!</p>
`;
}
}