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:
import i18next from 'i18next';
// Initialization, performed exactly once in the asynchronous function that starts the application
const runApp = async () => {
await i18next.init({
lng: 'en', // Current language
debug: true,
resources: {
en: { // Texts in a specific language
translation: { // The so-called namespace
key: 'Hello world!',
},
},
},
});
};
// We access the key somewhere in the application code
// The default library searches like this: <current language>.translation.<key> => en.translation.key
i18next.t('key'); // “Hello World!”
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:
import i18next from 'i18next';
// Just an example. The structure can be anything.
import en from './locales/en.js';
await i18next.init({
lng: 'en',
debug: true,
resources: {
en,
},
});
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.
{
translation: {
key: 'Hello world!',
signUpForm: {
name: 'Name',
email: 'Email',
}
}
}
i18next.t('signUpForm.name'); // Name
i18next.t('signUpForm.email'); // Email
In some situations, the texts may depend on various dynamic parameters, such as usernames. In this case, we use built-in interpolation:
{
translation: {
greeting: 'Hello {{name}}!',
}
}
i18next.t('greeting', { name: 'John' }); // "Hello John!"
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!
{
translation: {
{ // Interpolation is optional, it depends on the task
// Key names don't correspond to specific numbers
// They denote different groups of numbers https://jsfiddle.net/6bpxsgd4
key_one: '{{count}} score',
key_few: '{{count}} score',
key_many: '{{count}} score',
}
}
}
i18next.t('key', { count: 0 }); // "score"
i18next.t('key', { count: 1 }); // "score"
i18next.t('key', { count: 2 }); // "score"
i18next.t('key', { count: 5 }); // "score"
Linking texts to the application state
A typical mistake when working with texts is storing them directly in the state:
// Errors are just one possible example
// The same applies to any other texts
if (!isEmailUnique) {
state.signUpForm.errors.email = i18next.t('signUpForm.errors.email.notUnique');
}
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:
// Somewhere in the view
if (state.registrationProcess.finished) {
div.innerHTML = i18next.t('registration.success');
}
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 the translation file:
{
translation: {
key: 'Hello world!',
signUpForm: {
name: 'Name',
email: 'Email',
errors: [/* here are translations of errors */]
}
}
}
// Somewhere in the appendix
const state = {
signUpForm: {
valid: false,
errors: {},
}
};
// Somewhere in the handler
if (!isEmailUnique) {
state.signUpForm.errors.email = 'signUpForm.errors.email.notUnique';
}
// Somewhere in the view
div.innerHTML = i18next.t(state.signUpForm.errors.email);
In any case, finished strings are only formed when outputting.
Initializing
Let's go back to the example from the beginning of the lesson:
import i18next from 'i18next';
const runApp = async () => {
// Changes the i18next object globally
await i18next.init({
// Configuration i18next
});
app(); // inside the application, i18next.t now works with the selected language and loaded translations.
};
// We access the key somewhere in the application code
i18next.t('key');
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:
import i18next from 'i18next';
const runApp = async () => {
// The global i18next object does not change, and each run of the application will be independent of the others.
const i18nextInstance = i18next.createInstance();
await i18nextInstance.init({
// i18next configuration
});
// the initialized instance must be passed to the application
app(i18nextInstance);
};
// We access the the somewhere in the application code
i18nextInstance.t('key'); // "Hello world!"
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.
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.