I recently had to setup internationalization on my website Feeka.studio which is only built with HTML/SCSS/StimulusJS and a few JS libraries.
Surprisingly enough, I didn't find much material on how to easily implement it with a Stimulus controller and I spent some time researching in order to make it work. I hope this short walkthrough will help other people in the same situation !
The HTML
For this example, I created a very simple layout with a banner containing a language switcher and a catchline, along with a section holding some random content. I also sprinkled a bit of CSS on it to make it a bit more organized, everything is in the Codepen at the end of the article.
Here is what it looks like :
<section class="banner">
<div class="switcher" data-controller="locale"></div>
<div class="catchphrase" data-i18n="[html]catchphrase"></div>
</section>
<section class="content" data-i18n="content">
</section>
Setting up our Stimulus controller
For this example, I will be creating a single "LocaleController". Keep in mind that I will be putting all of my Javascript in the same file here since I'm using Codepen, but it's always good to split the different parts of the application, for example the controller should be declared in its own file, and the i18n configuration we'll be writing later on should be in another one. Being organized and maintaining the separation of concerns makes our lives easier in the long run !
Here is the basis of my controller :
class LocaleController extends Controller {
static targets = ['language'];
get languages() {
return [
{ title: 'FR', code: 'fr' },
{ title: 'EN', code: 'en' }
];
}
initialize() {
this.element.innerHTML = this.languages.map((locale) => {
return `<span data-action="click->locale#changeLocale"
data-locale="${locale.code}" data-target="locale.language">${locale.title}</span>`
}).join('');
this.setLocale('fr');
}
changeLocale(e) {
this.setLocale(e.target.getAttribute("data-locale"));
}
setLocale(locale) {
this.highlightCurrentLocale(locale);
}
highlightCurrentLocale(locale) {
this.languageTargets.forEach((el, i) => {
el.classList.toggle("active", locale !== el.getAttribute("data-locale"))
});
}
}
First of all, I'm defining a getter for our list of languages which for this example will be French and English. The title
represents the text that should appear in the language switcher and the code is what we'll use to manage our translations with i18next later on, it could also be written using the standard 'fr-FR' notation.
In the initialize()
function, I'm setting up my dynamic language switcher by iterating over the available languages and inserting a span for each of them, along with a data-action attribute that will call our changeLocale()
function on click and a data-locale attribute that will make it easy to retrieve the language code when the element is clicked. I'm also manually setting the locale to French at the moment but that will be handled by i18next once we implement it.
Right now the changeLocale(e)
function only makes sure we hide the current language in the switcher and show the other one. Using CSS, I made sure only the with the 'active' class is shown.
Here is the current state of things : https://codepen.io/martinvandersteen/pen/vYgEEMN
We just have a language switcher that switches when you click it, nothing crazy, but that'll change quickly !
Adding i18next in the mix
For this, I'm using some additional packages : 'i18next' that manages the bulk of the i18n job, 'loc-i18next' that will insert the translations in our HTML to make it a bit easier for us and 'i18next-browser-languagedetector' that does exactly what the name suggests ;)
Initializing our packages
At the top of my file, I'll be creating simple objects like these to make it easy to see on CodePen :
const frTranslations = {
catchphrase: "Bienvenue sur <strong>DEV.TO</strong>",
content: "Contenu statique de mon site internet"
};
const enTranslations = {
catchphrase: "Welcome to <strong>DEV.TO</strong>",
content: "Static content of my website"
};
In the production environment I'm putting all of my translations in two /locales/fr/global.json
and /locales/en/global.json
files, then I'm simply importing them when I'm initializing i18next, that makes it all a bit cleaner. But that'll do just fine for the sake of the example !
You can see that the "catchphrase" and "content" keys are actually the ones used in the [data-i18n]
attributes in our HTML, that's what enables our 'loc-i18next' package to know where to insert the various translations in our file.
After writing down those translation objects, let's initialize i18next like so :
// We're telling i18next to use(LngDetector) which is the name I used to import our 'i18next-browser-languagedetector' package
const i18n = i18next.use(LngDetector).init({
supportedLngs: ['fr', 'en'], // The list of languages we use
fallbackLng: 'en', // The default language to use when no translations are found in the current locale
detection: {
order: ['navigator'] // What to infer the initial locale from, this is given by our LngDetector
},
resources: {
fr: { translation: frTranslations }, // Specifying our translations
en: { translation: enTranslations }
}
}, function (err, t) {
if (err) return console.error(err) // Writing down errors in the console if need be
});
We can then initialize 'loc-i18next' which will insert our translations in the DOM by specifying the HTML attribute we used to mark the places used for the content :
// We attach localize to our i18next instance and tell him to look for 'data-i18n' attributes
const localize = locI18next.init(i18next, {
selectorAttr: 'data-i18n'
});
With everything setup, it's time to insert our i18next logic into our controller and make it all work together !
Updating the Controller
In our Initialize method, we'll simply wrap everything with our i18n.then(() => {});
call, that will make sure we only run that code after i18n has been fully initialized with the translations and current browser language, like so :
initialize() {
i18n.then(() => {
this.element.innerHTML = this.languages.map((locale) => {
return `<span data-action="click->locale#changeLocale"
data-locale="${locale.code}" data-target="locale.language">${locale.title}</span>`
}).join('');
this.setLocale(i18next.language);
});
}
Notice we're also setting the locale at the end of the function with setLocale(i18next.language)
, using the language automatically detected by our i18next LngDetector as argument.
Inside setLocale(locale)
, we'll make sure that we change the locale directly at the i18next level now, and we'll call the localize(selector)
method from 'loc-i18next' in order to update the content according to the new language.
setLocale(locale) {
i18next.changeLanguage(locale).then(() => {
localize('[data-i18n]'); // localize() takes as argument a selector, by passing '[data-i18n]' we update all DOM elements that have a data-i18n attribute set
this.highlightCurrentLocale();
});
}
And we also have to update the "highlightCurrentLocale()" function so that it uses "i18next.language" to define the current locale used.
Conclusion
And voilà ! It is a pretty simple setup, so don't hesitate to build a bit on that basis, by changing part of the URL when the local changes and infering the locale from the URL/Browser cache/... You will find quite some documentation regarding the i18next package, even though a lot of it is about react-i18next, it still applies.
Here is the final codepen link : https://codepen.io/martinvandersteen/pen/abpbvgK
I hope it will help you setting up i18n on your own websites, cheers !
Top comments (0)