Text management and localization in a web application
Good day, habravchane!
In this article I will talk about a simple solution to the problem of managing text and localization in a web application, which you can implement yourself or use a ready-made one.
I have long wanted to share my own thoughts and experiences ... and, of course, talk for life.
Obviously, solutions for managing text and localization already exist, but they did not suit me for various reasons: cumbersome, inconvenient to use, not suitable, does not correspond to my vision of solving this problem, lacks functionality.
In addition, I do not really like third-party libraries because of their tendency to grow (this is when we need only a small part of all the functionality).
The company in which I work has its own solution, but, in my opinion, it is also far from ideal. And the need for backward compatibility with older versions makes it unnecessarily complicated.
At some point, I wanted something simple, easy, understandable and infinitely expandable for different tasks.
Formulation of the problem
Everything seems to be clear here. Or not? Let's think what we would like.
We need to somehow get localized texts. Texts may contain variables. Can variables be localized too ?! In theory, yes. And if the variable is a date or a number ?! Plus markdown support. And finally, some solution in case the text is not found.
Implementation
The basis will be a simple object, where the key is the code of the text, and the value is the actual text you need, nothing complicated:
const textsBundle = {
'button.open': 'Open',
'button.save': 'Save',
};
function TextManager(texts) {
this.getText = function(code) {
return texts[code];
};
}
const textManager = new TextManager(textsBundle);
textManager.getText('button.open');
The name of the keys is a separate topic. It’s better to immediately agree on any one option, otherwise different keys will be "jarring" :). There is no one solution, choose what you think is more convenient and more consistent with the project. Personally, I like the first one: 'button.open.label'
'button.open.help_text'
either 'button.label.open'
'button.help_text.open'
or'label.button.open'
'help_text.button.open'
Next, we need a mechanism that would be able to perform some kind of manipulation of the text before it gives the final result, for example, insert parameters. And then an interesting idea came to me - what if we use middleware to manipulate text? After all, I have not seen such decisions ... well, or I’ve looked badly :).
We decide on the requirements for middleware: at the input, middleware will accept text and parameters, and output - the resulting text, after the necessary manipulations.
The first middleware will receive the original text, and the subsequent ones will receive the text from the previous middleware. Let's add the missing code:
function TextManager(texts, middleware) {
function applyMiddleware(text, parameters, code) {
if (!middleware) return text;
return middleware.reduce((prevText, middlewareItem) => middlewareItem(prevText, parameters, code), text);
}
this.getText = function(code, parameters) {
return applyMiddleware(texts[code], parameters, code);
};
}
TextManager can output text by its code. It can also be expanded using middleware, which opens up many possibilities, for example:
- handling the case when the text is not found
- use of parameters in the text
- parameter localization
- use markdown
- text shielding, etc.
Practice
We will write a couple of necessary middleware. You will need them 100%.
InsertParams
Allows the use of parameters in texts. For example, we need to display the text "Hello {{username}}". The following middleware will provide this:
function InsertParams(text, parameters) {
if (!text) return text;
if (!parameters) return text;
let nextText = text;
for (let key in parameters) {
if (parameters.hasOwnProperty(key)) {
nextText = text.replace('{{' + key + '}}', parameters[key]);
}
}
return nextText;
}
UseCodeIfNoText
Returns the code of the text, instead undefined
, if the text was not found:
function UseCodeIfNoText(text, parameters, code) {
return text ? text : code;
}
Total we receive approximately the following use:
const textsBundle = {
'text.hello': 'Hello',
'text.hello_with_numeric_parameter': 'Hello {{0}}',
'text.hello_with_named_parameter': 'Hello {{username}}',
};
const textManager = new TextManager(textsBundle, [InsertParams, UseCodeIfNoText]);
textManager.getText('nonexistent.code') // 'nonexistent.code'
textManager.getText('text.hello') // 'Hello'
textManager.getText('text.hello_with_numeric_parameter', ['Vasya']) // 'Hello Vasya'
textManager.getText('text.hello_with_named_parameter', { username: 'Petya' }) // 'Hello Petya'
React Application Example
First, initialize at the top level TextManager
and add texts.
In my opinion, it is best to pull texts from the server, but for simplicity I will not do this.
const textsBundle = {
'text.hello': 'Hello {{username}}'
}
function TextManagerProvider({ children }) {
const textManager = new TextManager(textsBundle, [InsertParams, UseCodeIfNoText]);
return (
{children}
)
}
Further in the component we use textManager
, for example, using a hook, and we get the desired text by code.
function SayHello({ username }) {
const textManager = useContext(TextManagerContext);
return (
{textManager.getText('text.hello', { username })}
)
}
Localization
You ask, "What does localization have to do with it?".
Everything is very simple - when changing the language, create a new instance TextManager
, add texts and immediately get the result.
The penultimate chapter :)
As you can see from the examples, the use is extremely simple, and thanks to middleware you can extend the functionality indefinitely.
I posted my implementation on github and plan to further develop the text-manager . Take advantage, propose improvements and, as they say there, You're welcome! :)
Finally
So I fulfilled my daaaaavnee desire - wrote an article on Habr. I really hope that this article will be useful and will appeal to the community.
Thank you for your attention.