Add language support to your Stencil JS app with PolyglotJS

5 Jun

Stencil Localization

Localization:

Localization or l10n in an application is slowly becoming a necessity for all modern client-facing web applications, in the same way as Accessibility is. Language is very close to one’s heart, the medium in which a person thinks and dreams in. If people can use our application in their own language, it guarantees a much better outreach and a kind of personal connection with them. We will look at how to provide localization, or locale based language support in a Stencil JS application in this blog post. Locale is the region where you are accessing a particular application from, usually that also means the local ‘language’, and depicted by standard prefixes such as ‘en’, ‘de’, ‘ar’ etc.

Stencil JS has gained a lot of popularity these days. It allows us to build reusable ‘web components‘ and ship them around as Node packages. This helps us in implementing portability of our code as well as assure a separation of presentation logic from business logic, meaning, our components can purely be skeletons to hold data as well as define the look, while the framework that we use them with will hold the business logic.

Prerequisites:

We will need npm version 6+ and the latest LTS Node, which right now is 14. Let’s create a Stencil JS application first. We’ll follow this doc to do that: Getting started. This post assumes that you have a fair understanding of JavaScript, and preferably Stencil JS, as well as an idea of what localization is and when to use it.

Starting up:

Run the following command to get started:

npm init stencil

(follow the instructions that npm displays)

This will set up Stencil and all its dependencies, as well as create a sample component for us. Here is how it will look:

import { Component, Prop, h } from '@stencil/core';
import { format } from '../../utils/utils';

@Component({
    tag: 'my-component',
    styleUrl: 'my-component.css',
    shadow: true,
})
export class MyComponent {
    /**
     * The first name
     */
    @Prop() first: string;

    /**
     * The middle name
     */
    @Prop() middle: string;

    /**
     * The last name
     */
    @Prop() last: string;

    private getText(): string {
        return format(this.first, this.middle, this.last);
    }

    render() {
        return (
            <div>Hello, World! I'm {this.getText()}</div>
        )
    }
}

Very soon we will change this to be translated. Let us now install PolyglotJS. We will use a package called ‘node-polyglot’ for our purpose.

npm i node-polyglot

We have the playground ready, now we can begin with the actual work.

The Idea:

We are going to detect the ‘locale’ for our browser on app init, then load the appropriate language JSON file as an environment variable that will be shared across our application. A component can then use an exported Polyglot instance, which in turn will use the language environment variable to return the translated string. Sounds easy, right?

Well, actually, there is a bit of work we need to do here! Our first hurdle is with Stencil JS. Stencil has no way of providing a global shared variable using something like a ‘Service’ in Angular, or a ‘Context’ in React, or a ‘Provider’ in VueJS, since it is more of a component library (feel free to correct me). Sure, there is ‘State Tunneling’ but that comes into picture at a runtime level. However, we need to execute code when the application first runs or loads. Thus we are going to resort to some fundamental JavaScript to achieve this. A ‘Singleton’ service, or a class that allows us to store ‘config’ like data on the Window object is what we need. Let’s do that!

Detecting locale and loading the JSON:

Create a new folder named ‘global’ in the ‘src’ folder, and create a new file in this folder called ‘config-service.ts’. Put the following code in it:

import {AppConfig} from './config';

export class AppConfigService {

    private static instance: AppConfigService;
    private m: Map<keyof AppConfig, any>;

    private constructor() {
        // Private constructor, singleton
        this.init();
    }

    static getInstance() {
        if (!AppConfigService.instance) {
            AppConfigService.instance = new AppConfigService();
        }
        return AppConfigService.instance;
    }

    private init() {
        if (!window) {
            return;
        }

        const win = window as any;
        const appConfig = win.appConfig;
        this.m = new Map<keyof AppConfig, any>(Object.entries(appConfig.config) as any);
    }

    get(key: keyof AppConfig, fallback?: any): any {
        const value = this.m.get(key);
        return (value !== undefined) ? value : fallback;
    }
}

This class is nothing but our ‘AppConfig’ service. All it does allows us to access the ‘appConfig’ object from Window and returns the value we need from it by the key name.

Now we need a way to set the data on Window. Lets go ahead and do that. Create a new file named ‘config.ts’ in the ‘global’ folder, and add the following code to it:

// The interface which define the list of variables
export interface AppConfig {
    lang: object;
}

export function setupConfig(config: AppConfig) {
    if (!window) {
        return;
    }

    const win = window as any;
    const appConfig = win.appConfig;

    if (appConfig && appConfig.config && 
        appConfig.config.constructor.name !== 'Object') {
        console.error('appConfig config was already initialized');
        return;
    }

    win.appConfig = win.appConfig || {};
    win.appConfig.config = {
        ...win.appConfig.config,
        ...config
    };

    return win.appConfig.config;
}

As you can see, we simply initiate ‘appConfig’ on the Window object, and set some config data on it. As we are creating a ‘Singleton’ we willnot allow creation of a second instance of the AppConfig class.

Reference: AppConfig service for sharing environment variables in Stencil JS

Stencil provides a ‘globalScript’ config option, that allows us to run code once before our app loads. What we can do is, use this script to do 3 things:

1. Detect locale
2. Load lang JSON
3. Set the lang strings as config, using the AppConfig service we created above

Let’s write that script. Create a new file named ‘init.ts’ inside our ‘global’ folder. Put the following code in it:

import { setupConfig } from  './config';

const getBrowserLang = (): string | undefined => {
    if (typeof window === 'undefined' 
        || typeof window.navigator === 'undefined') {
      return undefined;
    }

    let browserLang: string | null =
        window.navigator.languages && window.navigator.languages.length > 0 ? 
            window.navigator.languages[0] : null;
    // @ts-ignore
    browserLang = browserLang || window.navigator.language || window.navigator.browserLanguage || window.navigator.userLanguage;

    if (typeof browserLang === 'undefined') {
        return undefined;
    }

    if (browserLang.indexOf('-') !== -1) {
        browserLang = browserLang.split('-')[0];
    }

    if (browserLang.indexOf('_') !== -1) {
        browserLang = browserLang.split('_')[0];
    }

    return browserLang;
}

const fetchLocaleStringsForComponent = (locale: string): Promise<any> => {
    return new Promise((resolve, reject): void => {
      fetch(`/lang/${locale}.json`, {
        headers : { 
          'Content-Type': 'application/json',
          'Accept': 'application/json'
         }
       })
      .then((result) => {
        if (result.ok) resolve(result.json());
        else reject();
      }, () => reject());
    });
}

const locale = getBrowserLang()

try {
    await fetchLocaleStringsForComponent(locale).then(r => setupConfig({
        lang: r
    }))
} catch(e) {
    await fetchLocaleStringsForComponent("en").then(r => setupConfig({
        lang: r
    }))
}

We first detect the locale using the ‘getBrowserLang’ function, then use the ‘fetchLocaleStringsForComponent’ function for loading the appropriate lang.json file. Once the content of this file is available, we call the ‘setUpConfig’ function we created earlier to set the lang config on the Window object. This config is nothing but a JavaScript ‘object’ or key – value pairs, and we are going to use the keys to get the translated equivalents in our components. We are providing a fallback to ‘en’ or English.

References:
For locale detection
For loading locale JSON files

We will now call the above ‘init’ script at our Stencil app load time. Open up the ‘stencil.config.ts’ file, and add the following entry:

globalScript: 'src/global/init.ts'

The init script will be called on app load, and it will detect the locale and set the language JSON as an environment variable on the Window object. We still haven’t created the actual language files. Lets do that. Create a new folder named ‘lang’ inside the ‘src’ folder, and add the following two files:

en.json

{
    "hello": "Hello, world!",
    "rant": "I'm Stencil 'Don't call me a framework' JS"
}

hi.json

{
    "hello": "नमस्कार, दोस्तों!",
    "rant": "मेरा नाम स्टैंसिल हैं, कृपया मुझे फ्रेमवर्क न बुलाये :)"
}

If you notice, the names of the files are the locales that we detect using our ‘globalScript’, on app load. Remember, they keys in these JSON files are very important, as they will give you the actual string to be displayed in the browser. We can create any language file here, but we need to make sure the language code or prefix is correct. For example, if you need to add support for German language, you should have a ‘de.json’ file that will contain the translations for our strings in German, but with the same keys. Here is a list of locale code in Windows for your reference: Locale Codes

Using in our actual component:

We have our primary set up ready. The language is ready to be used! Now we need to use Polyglot to actually put the language JSON to use. Create a new file named ‘localize.ts’ in our ‘utils’ folder, ad paste the following code in it:

import Polyglot from 'node-polyglot'
import { AppConfigService } from '../global/config-service'

const polyglot = new Polyglot()
const lang: string =  AppConfigService.getInstance().get('lang');

polyglot.extend(lang);
export default polyglot;

What we are doing is, using our AppConfig service to get the ‘lang’ config we set during the app load, and pass it to Polyglot’s extend function. Export this instance of Polyglot to be used in our actual components. Open up the ‘my-component.tsx’ file that Stencil created for us, and paste the following in it:

import { Component, Prop, h } from '@stencil/core';
import poly from '../../utils/localize'

@Component({
  tag: 'my-component',
  styleUrl: 'my-component.css',
  shadow: true,
})
export class MyComponent {
  render() {
    return 
        <div>{poly.t('hello')} {poly.t('rant')}</div>;

;
  }
}

Fairly straightforward, isn’t it? We are simply importing the Polyglot instance from our ‘localize.ts’ file, and using it’s ‘t’ function to translate the strings we will be displaying in the browser. Since Polyglot is a default export, you can call it anything you want, I called it poly as in Polyglot.

There is one last detail to be completed. We are loading the language files asynchronously using the fetch API. Which means, they will need to be available in a folder that can be accessed relative to the home URL. For this, we need to run a copy job when we build our Stencil app. Open the ‘stencil.config.ts’ file again and add the following to the ‘outputTargets’ entry:

{
    type: 'www',
    serviceWorker: null, // disable service workers
    copy: [
        { src: 'lang' }
    ]}
You may have a ‘www’ entry already, simply add the ‘copy’ command to it. Please not that the service worker setting is not related to what we are trying to do, and it may differ in your environment.

Running it all:

Lets test it out. Run the application using npm start. It will open up in your browser. Assuming your browser’s default language is English, you should see the website using the strings in our ‘en.json’ file. Here is how it looks for me:

Localization with en.json

Now lets change the language. Your browser’s settings should allow you to do it. I am using Bing, in the settings, you may need to add the language you want if it isn’t available, and move it to the top of the list. Here is how my application looks with the language or locale set as ‘Hindi’:

Localization with hi.json

Hence we have achieved a basic level of localization for our Stencil JS application. The possibilities are endless from here. You can use this as a starting point and build your own system further. One idea would be to prompt the user to change the language, and set the language variable in the local storage. Then use that language to choose the JSON file to be used to display strings. In this case however, you can use State Tunnel to share the state, as you would be in the run phase of your application. This will allow only your application to have a different language, instead of relying on the browser locale.

I had a lot of fun doing this, hope it helps you in getting started with l18n for your apps. Please feel free to correct me wherever I am wrong or inaccurate or you think something can be improved, I am not an expert, I am standing on the shoulders of giants, just like all of us!

Thanks!

 

No comments yet

Leave a Reply