JS: Frontend architecture
Theory: Organization of interface texts
Any site's interface will have both visual and text components. This could include the names of the buttons, menu items, error messages in the form, and various texts scattered throughout the site.
The important thing is that they're not stored in a database, but are instead integrated directly into the code where they're used.
These texts can end up being a real pain. They sprawl over all the layers of the application and clog it up. The same phrases can end up being duplicated very quickly. It becomes difficult to monitor them and make sure they're consistent and adequate. The result is that the programmers will end up being the only people in the company who can change them, since no one else understands how to find these texts but them.
With the right approach, such texts can be kept in one place separate from the code. This way makes the application much more simple:
- The texts get easier to manage, update all together, and keep track of what's out of date.
- It's not just programmers who can do this. Moreover, texts can be uploaded to external systems that allow many people to work with them (more about this below).
- Internationalizing and localizing them gets simpler, too.
So, how do you organize text storage? The answer for many programmers may seem surprising. Even if your site isn't planned to be multi-lingual, i18n libraries are still used for texts. i18n stands for internationalization. This term in programming refers to anything related to translation. As a rule, this refers to special libraries that allow you to translate interfaces, keeping the application code simple.
The most interesting thing is that these libraries provide all the features that were listed earlier and don't necessarily require programmers to add multiple languages. Think of multilingualism as a nice addition that you can use if you need to. In addition to basic tasks, these libraries also solve many related problems. We'll look at them below.
Hexlet has a large number of projects open on GitHub in various languages. All of these projects have texts, and they're all put into the code using i18n libraries. Most of these libraries are integrated with frameworks and come out of the box.
Each of these projects has a different way of organizing translations, as you can see from the different file formats. One thing remains unchanged; the lines are not scattered over the code. They are all collected in one place and put in the right places via the i18n libraries.
In the JS world, i18next has become the most popular library for working with texts. It's not just a library, it's a whole framework with integrations with all popular solutions like Angular, React, or Vue.js. Here's an example of its use:
The only place where the concept of “language” appears is in initialization. You need to specify the current language (lng) and add texts for that language. That's it: from here on, we just need to manage the texts. If new text appears, a key is created for it and added to the translation object. This text is then retrieved using the specified key. You can see from the code above that this text is straightforward to reuse. It is sufficient to refer to the same key elsewhere in the program.
When there's more text, you can put it in a separate file. In this case, the initialization changes to this:
In principle, it's better to take the texts out at once. There's never a shortage of them.
i18next supports a concept called “backends”. It allows you to load texts from external sources, for example, via an AJAX request (which is why the initialization of the library is asynchronous). Read more in the official documentation.
Over time, you'll notice that the flat key-value structure isn't always convenient. Sometimes, you'll want to use nesting and group keys. Fortunately, that isn't a problem. I18next supports this feature out of the box.
In some situations, the texts may depend on various dynamic parameters, such as usernames. In this case, we use built-in interpolation:
In more complex situations, interpolation alone is not enough. Imagine that we need to output a number of points like on Hexlet. The word "point" will change depending on the number of points: 1 point, 2 points, a million points. How do you do that? Using pluralization!
Linking texts to the application state
A typical mistake when working with texts is storing them directly in the state:
This approach has one very serious drawback. It's not compatible with switching languages. Imagine that the user has changed the interface language, and in the state at the time the texts are written. The following problem now arises: how do can you change the texts to the correct language? Generally speaking, you can't, because there's no information in the line of text about what language it was. I.e., it's not possible to match this text to a key and find the corresponding translation elsewhere. In addition, the task itself is very difficult, there can be many texts, and they're scattered all over different parts of the state. You have to write special logic for each specific situation (each specific part of the state).
Any texts that are output depending on user actions should not be stored in the application state. These texts must depend on the state of the processes:
Only in the few situations where you need to know explicitly which texts to use will you need store keys, for example, for error translation.
In any case, finished strings are only formed when outputting.
Initializing
Let's go back to the example from the beginning of the lesson:
The global object i18next is mutated during initialization, so the i18next.t function can be imported directly from the library. This is convenient from a usage point of view, but it adds problems when multiple initializations are required. When is it necessary to initialize the application more than once? One example is tests, where each test runs a new application from scratch, or it could also be needed in server-side rendering, where a different instance of the application is created for each user on the server. For such cases the library contains the createInstance function, which createInstance as you might imagine, a new instance of i18next:
This global state approach is not unique; for example, the axios library can be configured both globally and instantiate. In general, a mutable global state is evil and a source of bugs.

