Creating a language switcher in JavaScript
About 90% of my working days in the last 5 years has been making feature prototypes for a web application.
With any designs you create that have to work across different languages it’s not uncommon for different string lengths to create new design problems. Text you imagined would fit in space X no longer does in Greek or Italian etc.
There are number of fully featured JS powered translation libraries out there. This won’t be one of them!
I wanted to make something very simple. This is the kind of solution that is bread and butter for any seasoned JavaScript capable developer but I wanted to document a possible solution for the less experienced.
The requirements are minor; the ability to swap out particular strings of text in my prototype based on the language chosen from a drop-down.
Here is how we can do that.
The HTML side of things
First of all, we will make our language selector with a standard select
element. The internal option
elements are going to be populated by JavaScript. You can provide whatever id
you like for the select.
<select name="language" class="mb-POCControls_Selector" id="mbPOCControlsLangDrop">
</select>
Then at any place you would like to swap some text in a node, add the attribute data-mlr-text
. For example:
<span data-mlr-text>Sausages</span>
OK, that’s all you need to do in the HTML, everything else will be JavaScript.
JavaScript
We are going to have a main function called mlr
. We are going to invoke this function with a number of options like this.
mlr({
dropID: "mbPOCControlsLangDrop",
stringAttribute: "data-mlr-text",
chosenLang: "English",
mLstrings: MLstrings,
countryCodes: true,
countryCodeData: mlCodes,
});
Here is an explanation of the options we are going to need:
dropID
– the id of the select that will be used to chose languages (whatever you set in the HTML)stringAttribute
– the attribute name you will add to the nodes you want to switch the text on. Leave this as is unless you have a compelling reason to change it. In our example we useddata-mlr-text
chosenLang
is the initial language you want each string rendered in. The string you pass in should match the string in the language files (case sensitive)mLstrings
is the name of the variable/array you have all your translations stored incountryCodes
is an optional boolean value for if we want thelang
attribute on the HTML element updating toocountryCodeData
is an optional variable for an array containing country codes for your chosen language
Language files
To store our translations for each string, we can use an array of objects. Each object contains a relevant string in each of the languages to switch between. For example:
var MLstrings = [
{
English: "Sausages",
Polish: "Kiełbaski",
German: "Würste",
Russian: "Колбасные изделия",
Traditional: "香腸",
},
{
English: "Carrot",
Polish: "Marchewka",
German: "Karotte",
Russian: "Морковь",
Traditional: "胡蘿蔔",
}
];
You would add another object for each of the strings you need to switch out. I have added only two here to keep things simple.
/// <reference path="ts/mlStrings.ts">
Country Codes
The two character language codes for countries is defined in ISO 639–1. If you also want to update your HTML tag with the correct code for the chosen language you need to include a file containing the relevant country codes for the languages you are translating between. It should be formatted like this:
var mlCodes = [
{
code: "bg",
name: "Bulgarian",
},
// More country codes
];
Note that the casing of the country names should match the casing in your translation data.
Our string replacement function
Now lets look at the main functions that will drive our functionality. We will step-by-step it afterwards.
// Global var :(
var mlrLangInUse;
var mlr = function({
dropID = "mbPOCControlsLangDrop",
stringAttribute = "data-mlr-text",
chosenLang = "English",
mLstrings = MLstrings,
countryCodes = false,
countryCodeData = [],
} = {}) {
const root = document.documentElement;
var listOfLanguages = Object.keys(mLstrings[0]);
mlrLangInUse = chosenLang;
(function createMLDrop() {
var mbPOCControlsLangDrop = document.getElementById(dropID);
// Reset the menu
mbPOCControlsLangDrop.innerHTML = "";
// Now build the options
listOfLanguages.forEach((lang, langidx) => {
let HTMLoption = document.createElement("option");
HTMLoption.value = lang;
HTMLoption.textContent = lang;
mbPOCControlsLangDrop.appendChild(HTMLoption);
if (lang === chosenLang) {
mbPOCControlsLangDrop.value = lang;
}
});
mbPOCControlsLangDrop.addEventListener("change", function(e) {
mlrLangInUse = mbPOCControlsLangDrop[mbPOCControlsLangDrop.selectedIndex].value;
resolveAllMLStrings();
// Here we update the 2-digit lang attribute if required
if (countryCodes === true) {
if (!Array.isArray(countryCodeData) || !countryCodeData.length) {
console.warn("Cannot access strings for language codes");
return;
}
root.setAttribute("lang", updateCountryCodeOnHTML().code);
}
});
})();
function updateCountryCodeOnHTML() {
return countryCodeData.find(this2Digit => this2Digit.name === mlrLangInUse);
}
function resolveAllMLStrings() {
let stringsToBeResolved = document.querySelectorAll(`[${stringAttribute}]`);
stringsToBeResolved.forEach(stringToBeResolved => {
let originaltextContent = stringToBeResolved.textContent;
let resolvedText = resolveMLString(originaltextContent, mLstrings);
stringToBeResolved.textContent = resolvedText;
});
}
};
function resolveMLString(stringToBeResolved, mLstrings) {
var matchingStringIndex = mLstrings.find(function(stringObj) {
// Create an array of the objects values:
let stringValues = Object.values(stringObj);
// Now return if we can find that string anywhere in there
return stringValues.includes(stringToBeResolved);
});
if (matchingStringIndex) {
return matchingStringIndex[mlrLangInUse];
} else {
// If we don't have a match in our language strings, return the original
return stringToBeResolved;
}
}
I’ve got a globally accessible variable at the outset, mlrLangInUse
. I’ve namespaced it with mlr
to avoid conflicts but I’m still not massively happy about it. However, I couldn’t find an elegant away around it. I need the value that variable stores to be accessible to the resolveMLString
function which can be called independently. More on that function shortly.
The mlr
function starts by ‘de-structuring’ the options. This is the ES6 style of setting defaults for an options object, with the value to the right of the =
sign being the default setting for each option. Note that the option object itself gets a default to at the end with the = {}
part. Best explanation I came across of de-structuring was at https://simonsmith.io/destructuring-objects-as-function-parameters-in-es6/
Next we var
the HTML element in case we want to update the lang
attribute. Then we create an Array of the possible languages. We do this by looking at the first object in the mLstrings
and use the Object.keys()
method to create an array from the keys in the object.
We also set the language in use in the globally accessible mlrLangInUse
variable. This is initially set to the string passed in for chosenLang
.
const root = document.documentElement;
var listOfLanguages = Object.keys(mLstrings[0]);
mlrLangInUse = chosenLang;
Next we create the drop-down options inside an immediately invoked function (note the double brackets at the end).
Each option is appended to the select
element we specified in the options (the id was mbPOCControlsLangDrop
in our example).
With the options created, we then add an event listener to the select
element and use this listener to update things on the change
event.
Let’s cover what we do each time there is a change on the select
element.
mlrLangInUse = mbPOCControlsLangDrop[mbPOCControlsLangDrop.selectedIndex].value;
resolveAllMLStrings();
// Here we update the 2-digit lang attribute if required
if (countryCodes === true) {
if (!Array.isArray(countryCodeData) || !countryCodeData.length) {
console.warn("Cannot access strings for language codes");
return;
}
root.setAttribute("lang", updateCountryCodeOnHTML().code);
}
The first line in the above code re-assigns a value to mlrLangInUse
. If you remember this was set at the outset by the options. We want to update this to the value of the language assigned to the option
in the drop-down. selectedIndex
is part of the Web API and let’s you get at the chosen option from a drop-down. With that we can then read the value of that option.
Next we fire the resolveAllMLStrings()
function. But before we get into that lets go to the next lines where we also set the lang
attribute of the HTML element. First we check if countryCodes
is true. If not, we are done. If it is true, we next check if we have any data in the array of countryCodeData
or if it even exists. If not we send a warning to the console and return. Otherwise we go ahead and set the language attribute by running a one line function (updateCountryCodeOnHTML
) that returns the object from the countryCodeData
array that matches the currently selected language. The .code
part just uses the relevant country code part of the retrieved object. For example, I ‘Czech’ was selected I would be retrieving:
{
code: "cs",
name: "Czech",
},
So the code
part would be ‘cs’.
OK, back to the resolveAllMLStrings
function. First of all it goes and finds all the strings in the HTML that have the stringAttribute
we defined in the options. In our example this would be all “data-mlr-text” attributes. Then it runs a function on each of them, taking the current textContent and assigning it to originaltextContent
and using that to set what the new text will be. We do this by passing the original text to the resolveMLString
function along with the strings we want to search within (mLstrings
). We then set the textContent
of the element to be the new string.
Right, final thing we need to look at is the resolveMLString
function. Unless someone can offer an alternative, I think this function has to be external to our main mlr
function so that it can be called outside of mlr
. For example, if elsewhere we want to set the text of an element with JavaScript, we need to call this function without re-instantiating the mlr
function (which would in turn require passing in all the necessary options). I’d welcome any feedback here if there is a better way to handle this. However, at present it is possible to do this elsewhere resolveMLString("Sausages", MLstrings)
and I would be returned the correct string based on the language currently set in the global mlrLangInUse
variable.
In the absence of a better alternative, let’s consider what resolveMLString
actually does.
We want to return a translated string if we have one, otherwise return the original string that was sent in. But how do we actually search for that string? We use the find()
method of Array
. We use find()
on our mLstrings
and assign an array to stringValues
that contains values from each object of strings. For example, stringValues
might look like ["Sausages","Kiełbaski","Würste","Колбасные изделия","香腸"]
. We can then use the includes()
array method on this new array to return true
if the array contains the string we are interested in. Now we have found the correct object from our main mlStrings
array, we can return the correct string because we know the language we are interested in. Phew!
To exemplify, had we chosen Polish
as our language and we wanted Carrot
in this language we can pick it from this data easily:
{
English: "Carrot",
Polish: "Marchewka",
German: "Karotte",
Russian: "Морковь",
Traditional: "胡蘿蔔",
}
Which is where we use matchingStringIndex[mlrLangInUse]
, otherwise we would just return back the original string.
Conclusion
OK, it might not look like much but here is an example of our efforts!
https://codepen.io/benfrain/pen/GdKrVx
See the Pen Simple Language Picker by Ben Frain (@benfrain) on CodePen.
I’ll be honest, I found this blog post quite difficult. Not because the solution is anything fancy but because sometimes it is harder to explain code than it is to read it.
The rub however is that if you can’t read the code/language in the first place it’s hard to understand it! That’s the position I find myself in constantly when trying to learn JavaScript.
I hope any less experienced JavaScript developers found some of this useful.
For the more experienced devs, I’m always happy to hear how any of this could be improved.
Really good article. Like that it goes deep into the topic.
An unrelated question: even though my first language isn’t English, I noticed something funny in the title. The word “switcher” looks strange to me. Instead I would expect one of these words: switch, selector, dropdown, etc. So I’m just wondering, is it common in English to call something like this a “switcher”?