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.
- The
getLocale
function will evaluate the DOM to find alang
attribute to determine the active locale. If it cannot find alang
attribute, it will fall back to English. - 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. -
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.
Top comments (1)
How to resolve the translation file path, when component uses inside an app.