loading...
TeamHive

Using i18n Translations within Stencil Components

seanperkins profile image Sean Perkins ・3 min read

Introduction

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>
   );
}

Cons

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:

libs/
    i18n/
        en.json
    ui/
        stencil.config.js
        src/

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'
    }
],

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"
}]

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);
            }
        }
    }

}

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;

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

async componentWillLoad() {
    this.translations = await TranslationUtils.fetchTranslations();
}

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>
    );
}

Advantages

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.

Discussion

pic
Editor guide