Translations in plain JS
So, you want to implement translations in your app, and you’re shopping around for an i18n library?
Stop! Before you npm install
anything and bloat your app, consider the key question:
Do you need another package?
Probably not.
“But i18n libraries are so complex.”
Modern libraries come with many bells and whistles you probably don’t need. Specially if translations are “just a nice to have”.
“Translations are, at best, an extra feature we don’t actually need. We will not spend developer hours re-inventing the wheel.”
But you want to spend developer hours managing npm package compatibility issues? And de-bloating your app? And patching vulnerabilities of second-hand dependencies you didn’t even install?
Implement your own i18n parser in-house. It isn’t hard, at all. It might take a mature developer a few hours, and is a fine learning project for a new-comer.
A simple implementation of i18n
The key elements you might want to deal with:
- A data structure to store your translations
- Base entry function: to get your translated string
i18n("key")
- A way to store (and change) language setting
- (optional) Object navigation: to have multi-layer translation objects
- (optional) String to object interpolation: variables inside your strings
Store your translations
Here’s a standard i18n format. This structure will make it easier to access keys by language. Make sure that keys are the same across languages.
const translations = {
en: {
greet: "Hello World",
},
es: {
greet: "Hola Mundo",
},
};
You will have to define if you want all your translations together in one .json
file, or in-line with your app, one object per component, or any hybrid between. There’s no right answer.
Base entry
You will need a base function to retrieve the translation string given a key. This is straightforward: just an object lookup. Some standard names for this entry function are _("key")
, t("key")
, i18n("key")
. Choose one and be consistent.
function i18n(key) {
return translation[language][t];
}
Store language choice
Odds are, you already have a store (redux, vuex, pinia), a singleton object, or you’re just using the localStorage
. No need to be creative, just follow the normal flow of your app.
Remember to set a default, and to allow the user to edit this choice.
It’s not a bad idea to have something like this:
function i18n(key) {
language = language ?? "en";
return translation[language][t];
}
Object navigation (optional)
If you want to have a multi-layer object to save your translation strings, you will need to navigate the object based on the keys.
const translations = {
"en": {
"welcome": {
"greet": "Hello {name}",
},
...
},
...
}
// Pass the key as a path
i18n("welcome.greet");
// Here's an implementation
/**
* Navigates inside `obj` with `path` string,
*
* Usage:
* objNavigate({a: {b: 123}}, "a.b") // returns 123
*
* Returns null if variable is not found.
* Fails silently.
*/
function objNavigate(obj, path){
patht = path.split('.');
try {
return path.reduce((a, v) => a[v], obj);
} catch {
return null;
}
};
String interpolation (optional)
If you want your translations to handle variables, like
const translations = {
"en": {
"greet": "Hello {name}",
},
...
}
// And pass the variable as an arguement
i18n("greet", {name: user.name});
// Here’s an implementation. Feel free to change the regex to suit your preference.
/**
* Interpolates variables wrapped with `{}` in `str` with variables in `obj`
*
* Usage:
*
* named variables:
* strObjInterpolation("I'm {age} years old!", { age: 29 });
*
* ordered variables
* strObjInterpolation("The {0} says {1}, {1}, {1}!", ['cow', 'moo']);
*/
function strObjInterpolation(str, obj){
obj = obj || [];
str = str ? str.toString() : ';
return str.replace(
/{([^{}]*)}/g,
(a, b) => {
const r = obj[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
},
);
};
Remember to add these optional arguments to your base entry function.
A good practice
Fail gracefully. You will be showing these strings to the user, no need to be too fussy.
- If language not found, try the default language.
- If key not found, return the key. Hopefully your key will be descriptive enough for the median user to get the gist of it.
That’s it
No config file, no object initialization, no server, no extra dependencies, no package version, no bloat.
You can implement this in vue, react, svelte, with typescript, as a util function, as a mix-in, or as a global pre-loaded component. You can continue adding functionalities, just be mindful of your needs first.
Photo by Romain Vignes on Unsplash