DEV Community

Sean Perkins for TeamHive

Posted on


Using i18n Translations within Stencil Components


Supporting multiple languages is a common requirement for developing websites and applications. Using Stencil we are able to build extendable web components in a design system to expedite application development.

Unfortunately, any components composed of translatable text often times are forgotten or worse; additional mark-up is added to a web component to allow for it to accept a translated label.

Example of ineffective solution:

@Prop() ofLabel: string;

render() {
   return (
       <span>5 {this.ofLabel} 10 Results</span>
Enter fullscreen mode Exit fullscreen mode


Anywhere that I use this component, I will need to be aware of the translation property as well as remember to pass it in.

The Solution

If you are supporting translations in your application, you most likely already have a managed translation file for your application. We can reuse this file to manage our translations from a single point of truth!

In this example we are using an NX mono-repository from the app starter template. You can easily adapt the configurations to meet your project's needs.

Include Locales into Bundle

We have the following structure in our project:

Enter fullscreen mode Exit fullscreen mode

Update your stencil.config.ts file to copy the assets from the i18n library to include in your bundle.

copy: [
        src: '../../i18n/src/lib/*.json',
        dest: 'i18n'
Enter fullscreen mode Exit fullscreen mode

Copy Locales to Public Location

We are using Angular for our enterprise stack. In order for our stencil component to have a reliable location to parse translations from, we need to expose the assets as a public resource.

Update your angular.json for your project to include the following copy job for the assets config:

"assets": [{
     "glob": "**/*.json",
     "input": "libs/ui/dist/collection/i18n",
     "output": "i18n"
Enter fullscreen mode Exit fullscreen mode

Translation Utils

We will be parsing translations a lot for each individual component that is rendered. To prevent writing duplicate code as well as optimistically cache the translation data, we will be creating a translation utility class to help.

In libs/ui/src/utils create a new file translation.ts.

export namespace TranslationUtils {

     * Attempts to find the closest tag with a lang attribute.
     * Falls back to english if no language is found.
     * @param element The element to find a lang attribute for.
    function getLocale(element: HTMLElement = document.body) {
        const closestElement = element.closest('[lang]') as HTMLElement;
        return closestElement ? closestElement.lang : 'en';

    export async function fetchTranslations() {
        const locale = getLocale();
        const existingTranslations = JSON.parse(sessionStorage.getItem(`i18n.${locale}`));
        if (existingTranslations && Object.keys(existingTranslations).length > 0) {
            return existingTranslations;
        } else {
            try {
                const result = await fetch(`/i18n/${locale}.json`);
                if (result.ok) {
                    const data = await result.json();
                    sessionStorage.setItem(`i18n.${locale}`, JSON.stringify(data));
                    return data;
            } catch (exception) {
                console.error(`Error loading locale: ${locale}`, exception);

Enter fullscreen mode Exit fullscreen mode

This utility focuses on solving three unique challenges.

  1. The getLocale function will evaluate the DOM to find a lang attribute to determine the active locale. If it cannot find a lang attribute, it will fall back to English.
  2. The fetchTranslations function optimistically fetches and writes to session storage to prevent multiple requests to the translation file. It caches per locale, to allow for changing between languages easily.
  3. The fetchTranslations function returns the contents of the locale file (i.e en.json) as a JavaScript object.

Component Implementation

In your existing web component, declare a state variable for the translations object.

@State() translations: any;
Enter fullscreen mode Exit fullscreen mode

In the life cycle hook for loading the component, utilize the translation utility to fetch the translations.

async componentWillLoad() {
    this.translations = await TranslationUtils.fetchTranslations();
Enter fullscreen mode Exit fullscreen mode

Now you will have the full collection of translations that are shared with your application. You can easily use any translation key as a JavaScript object.

render() {
    return (
         <span>5 {this.translations.of} 10</span>

Enter fullscreen mode Exit fullscreen mode


As you manage translation file for your application, the component will always remain in sync. You can also allow the application to easily change the translations without having to maintain separate versions of a web component.

Future Proofing

To take this implementation a step further, you can easily use a MutationObserver on the element with the closest lang attribute, to handle when a browser changes translations.

Top comments (0)

11 Tips That Make You a Better Typescript Programmer


1 Think in {Set}

Type is an everyday concept to programmers, but it’s surprisingly difficult to define it succinctly. I find it helpful to use Set as a conceptual model instead.

#2 Understand declared type and narrowed type

One extremely powerful typescript feature is automatic type narrowing based on control flow. This means a variable has two types associated with it at any specific point of code location: a declaration type and a narrowed type.

#3 Use discriminated union instead of optional fields


Read the whole post now!