Implementing Internationalisation (i18n) with Stencil 🌍

Christian Cook 🍪
Stencil Tricks

--

If your Stencil component has a requirement to display pre-defined text to the user, chances are you are hardcoding in your values for a single language. In this guide, we will learn how to add internationalisation (i18n) to your components and only load strings in the required language at run-time.

Getting started

Let’s get started by creating a brand new Stencil project. In this example we will use the Stencil component starter.

npm init stencil  ionic-pwa
app
> component

After you have created your project, open the source code in your favourite code editor.

Defining locale strings

First off, we need to create some strings which we can provide values for. This will be an object of strings which we will supply for each language supported. In the ./src/components/my-component folder, create a new file called my-component.i18n.en.json which will house our strings object for the English language.

In this file, we will need to start defining the property and value for each string. To get started, we will create two string definitions title and subTitle .

{
"title": "My Cool Component",
"subTitle": "This is the bees knees 🐝"
}

So now we have our English strings defined, we can go ahead and create strings for other languages we wish our component to support. Simply create another JSON file in your component folder but swap out the language code for the desired language in the file name. For example, if you wish to provide strings for the German language, create a new JSON file called my-component.i18n.de.json and populate it with the same object as before. Of course, replacing the English values with their German counterparts.

{
"title": "Meine coole Komponente",
"subTitle": "Dies ist die Biene Knie 🐝"
}

Disclaimer: I failed my German GCSE back in school so I used Google Translate to provide the above translations!

Creating the strings loader

Currently, the strings are in the project but don’t do anything… we need to load them into the component when it gets created. By loading the strings at run-time it ensures that we don’t load the strings for all available languages and only the language we need. As a result, we minimise the amount of network requests that get made and reduce the amount of data transferred to the client… leading to faster load times and happy users.

We will create a helper file in our project to house all of our string loader functionality. This will allow it to be accessible across components rather than coding it into a specific component. Create a file at ./src/utils/locale.ts which we will put in the future code chunks.

Getting the language of the component

To know which strings to load, we first need to know what locale we need. Luckily, the HTML specification has the lang property which is widely supported on DOM elements. By looking at this property we know which language the content should be displayed as. This is most widely used on the html element of the page.

However, in some pages, they might contain multiple languages… so we need to inherit the language from the component or its nearest parent. We can do this by using JavaScript’s closest function and finding the nearest element with lang set. This will traverse up the DOM tree all the way up to the html element until it finds something that matches.

function getComponentClosestLanguage(element: HTMLElement): string {
let closestElement = element.closest('[lang]') as HTMLElement;
return closestElement ? closestElement.lang : 'en';
}

In this example, if no matching element is found, we return en as our default language.

Fetching locale strings for the component

Now that we know what language we wish to load, we can create a fetch call to go and get the correct JSON for our component which we created earlier. Here we are looking inside a i18n folder with the name of our component and the chosen locale. We will create the contents of this folder in a later step, as the JSON needs to be bundled up with our distributable components during the compile phase.

function fetchLocaleStringsForComponent(componentName: string, locale: string): Promise<any> {
return new Promise((resolve, reject): void => {
fetch(`/i18n/${componentName}.i18n.${locale}.json`)
.then((result) => {
if (result.ok) resolve(result.json());
else reject();
}, () => reject());
});
}

Providing a fallback language

You may have noticed in the previous step there is a rejection step on the Promise. If the file is unable to be loaded or we encounter any other problems with loading our desired locale, it will error gracefully. This is where our fallback handler comes in handy and allows us to combine together our previous efforts above into a single entry function.

With the code below we are firstly getting the tag name of our component (At this point we are assuming the tag name is the same as the filename of the component) and the resolved language we should be loading.From there we attempt to fetch the strings for the component and language. If this fails, we then fall back to fetching the en strings.

By doing this, if a strings file is not present for one of your components in your library it will warn you in the developer console and fall back to a language which you are confident has strings for everything.

export async function getLocaleComponentStrings(element: HTMLElement): Promise<any> {
let componentName = element.tagName.toLowerCase();
let componentLanguage = getComponentClosestLanguage(element);
let strings;
try {
strings = await fetchLocaleStringsForComponent(componentName, componentLanguage);
} catch (e) {
console.warn(`no locale for ${componentName} (${componentLanguage}) loading default locale en.`);
strings = await fetchLocaleStringsForComponent(componentName, 'en');
}
return strings;
}

Copying the strings as build artefacts

Mentioned earlier, before we can start using the code we need to make sure the strings files exist in our compiled output so they can be obtained at run-time. To do this, we need to add a copy script to our ./stencil.config.ts file.

By using a glob pattern, we can look for all JSON files which contain our naming scheme and copy them across into the build output. In our fetch method, we are looking in the i18n folder to keep things tidy, so make sure we set the copy destination to this folder else the files will not be found when we attempt to load them.

{
...
copy: [{
src: "**/*.i18n.*.json",
dest: "i18n"
}]
}

Putting it all together

Finally, we now have all of our parts put together and can start using the loader within our component(s). In our component we will need to load the strings during component initialisation and a place to store them in memory.

In ./src/components/my-component/my-component.tsx we need to use the lifecycle method for componentWillLoad() which will fire before our component is loaded. By defining an async method here, it will wait for the asynchronous method to complete before continuing, so we know for sure that our strings have loaded before we begin using them.

Additionally, our function requires that we pass in the component reference so our helper function can work out what element it is and what language the component requires.

import { getLocaleComponentStrings } from '../../utils/locale';...@Element() element: HTMLElement;
strings: any;
async componentWillLoad(): Promise<void> {
this.strings = await getLocaleComponentStrings(this.element);
}

Now we need to render the strings in our component. We can simply modify our render function to output our title and subTitle values we set in our JSON at the start of the article.

render() {
return ([
<div>{{this.strings.title}} - {{this.strings.subTitle}}</div>
]);
}

This should now render out our title and sub-title in the desired language. To configure our component to use a specific language, we set the lang attribute directly on our component in index.html or wherever the component is used.

<my-component lang="en"></my-component>

Because we implemented the language resolver, our component is now smart enough to inherit the language from the closest parent element with a lang set on it.

<html lang="en">
<!-- other stuff on your page -->
<my-component></my-component>
</html>

This means you can also have components in multiple languages on the same page, as long as you are explicit with setting the language property in the correct places. For example we can have a page which is in English, but switch to German for our component in one place, and French in another.

<html lang="en">
<!-- other stuff on your page -->
<my-component lang="de"></my-component>
<my-component lang="fr"></my-component>
</html>

TLDR; Show me the results!

If you are impatient or wish to see everything brought together, here is a live example of this working on Code Sandbox:

In real life scenarios, you could use this in a multitude of places… as element titles, aria-labels or even references to image URLs.

Credit goes to Stencil Slack community members @corymc and @simonhaenisch for their methods of loading strings on the fly and @Matt S for putting together the awesome Stencil starter for Code Sandbox.

--

--

Christian Cook 🍪
Stencil Tricks

👨‍💻 Technical Director of @elixelofficial | 👾 Co-founder of @DigitalPlymouth | 🕸 Organiser of @Plymouth_Web | 🖤 Maker of Web Components