Introduction
As part of my project, I was faced with the task of making the company's current website multilingual. More precisely: to create the ability to quickly and easily translate the site into English, Polish, Italian, etc.
An Internet search showed that the existing options for creating a multilingual site are extremely cumbersome and inefficient. Connecting third-party libraries is often problematic, and tips for writing your own solution are associated with a large amount of uniform work.
Writing an alternative method of changing the locale took me only a few hours, and maintaining semantic unity minimizes changes with the subsequent addition of new pages.
The source files of the sample site with automatic translation can be downloaded
on githubExisting Alternatives
When the task first appeared in development, the first step, of course, was the study of ready-made solutions and tips on the forums on how to most easily and correctly implement the possibility of changing the locale. The most popular Internet resources for creating multilingual websites are offering the following solutions:
- Creating duplicate html blocks with text in different languages, of which only one is left active for the user, while the rest are hidden (display: none).
The obvious disadvantage of this method is the incredibly rapid increase in the code and the instantaneous loss of readability and maintainability of the code. In addition, this solution is vulnerable to errors in the text and scalability in terms of increasing the number of languages (hereinafter referred to as locales).
- Connecting a third-party machine translation service (such as google translate) with a large number of built-in languages and minimal changes in the source code of the page.
When the task first appeared in the task list, we used this method as the most obvious and convenient, however, experience with clients - native speakers from the United States and Israel showed that machine translation often makes mistakes when changing locale, and users of sites extremely sharply react to such translation errors. In the end, the strategic partners strongly advised to change the method of changing the locale, and this method had to be abandoned.
- Change language using js features or third-party libraries / frameworks, such as jQuery, based on searching and directly modifying DOM elements.
A feature of this approach is the search for a huge number of js selectors, the text inside which you need to replace. This approach may work well for small projects, but as the number of pages increases, the number of text replacement functions increases proportionally, which leads to a loss of efficiency in large projects.
Alternative solution
The basis of the approach that I propose as an alternative to the existing methods is, oddly enough, the base of the js code I wrote, which is generally trivial, but the rule is the design of selectors, which allows for flexible and simple translation of any number of pages into any language changes to the code base and unnecessary data duplication.
In changing the locale with an alternative approach, there are three main players:
- html page with an established rule for the design of block selectors with text
- general js service, the main task of which is to replace textContet DOM elements according to the rule for the design of selectors
- JSON file of locale containing structure with content of html blocks in all languages used when changing locale
Compliance with the design rules for selectable elements allows you to eliminate the need to change the js code of the locale change service, which is a big plus in terms of the scalability of the project.
The rule of building selectors
Most methods of changing the page locale (among the alternatives 1.3 and partially 2) imply the need to “mark” the variable html block in some way, as correctly by changing the class field. The same mechanism uses an alternative option.
The first step in the design of the selectors is to divide the source page into top-level functional blocks. On our company page are blocks:
Each block is given a code name, for example,
Menu
Business card (home)
Example of the service (example)
Partners (clients)
Scope of service (userfulBlock)
Examples of the service (examples)
Contacts and feedback (contacts)
After that, we further divide each block into smaller functional blocks, as is done when using the React library.
We assign our names to the selected areas and get the structure of the form:
menu
home main, description, buttons
example statistics, headline, description, buttons
clients buttons
userfulBlock headline, userfulCards, elseBlock
examples headline, cards
contacts headline, description, contacts, form
Further we continue this procedure until we reach the blocks containing the source text.
As a result, we get a ready-made JSON file structure of the locale, containing all the necessary texts to change the language. Also, based on this algorithm, the rule for building selectors is determined:
Each selector starts with the locale keyword and further, according to the dash case style, the names of all parent blocks are added, including the block containing the source text, for example, the example description in the first card will have the locale-example-cards-description selector
An example of the resulting json file locale can be seen
on githubService change locale
The locale change service is a module that contains the function to load a locale file.
loadLocale(defLang)
with optional parameter defLang - language set after loading the locale (default language), as well as the main function of changing the current locale
changeLocale(lang)
indicating the required language.
Load locale function
The locale download function uses a standard XMLHttpRequest request for data. The use of this standard standard is due to the desire to minimize the number of dependencies and the ease of use of the query. After receiving the locale file, a notification about data acquisition is displayed in the console, and the function of changing the locale to the default language is called if this language was passed to the function as an optional parameter. You can view the function code here:
function loadLocale(defLang) { var xhr = new XMLHttpRequest(); xhr.open("GET", 'http://localhost:3000/locale.json', true); xhr.onreadystatechange = saveLocale.bind(this); xhr.onerror = function () { console.log("no found page"); }; xhr.send(); function saveLocale() { if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) { locale = JSON.parse(xhr.responseText); console.log("locale loaded"); if(defLang) changeLocale(defLang); } } }
Locale change function
Data types
It is a recursive function whose main task is to crawl an object containing the page locale (using the DFS algorithm). Using recursion when building a function allows you to code the algorithm as simply and concisely as possible, but too much recursion depth can lead to stack overflow. Features of the workaround for this problem can be found on the forum of the same name, or by reading the relevant articles on habr.com.
The basis of the recursive function is the processing of 4 data types:
- a field containing a string of source text used to add to the page.
For example:
"main": "Продающий квест из вашего видео"
- a field containing an array of source lines used to add to
page. Such a field is necessary for creating lists whose elements can be changed.
order For example:
"menu":["Home","Example","Clients","Info","Contacts"]
- A nested data structure containing its own set of fields required for building
page architecture. For example:
"home": { "main": "selling quest from your video", "description": "for social networks & sites", "buttons": ["try","order"] }
- An array of nested data structures with the same set of fields used. Such
arrays are used when lists of identical blocks of code appear, for example,
cards of team members, or portfolio or tariffs of services provided.
For example:
"usefulCards": [ { "headline": "Marketers and agencies", "statistics": ["convers 26%", "retent 25%"], "button": "ORDER" }, { "headline": "Production studios and TV platforms", "statistics": ["convers 24%", "retent 33%"], "button": "ORDER" }, { "headline": "Conference creators", "statistics": ["convers 65%", "retent 15%"], "button": "ORDER" }, { "headline": "Bloggers and streamers", "statistics": ["convers 24%", "retent 33%"], "button": "ORDER" } ],
On the site, it might look like this:
Processing functions
Processing the data type with the source text is a separate function.
function getText(key, object, name,startIndex)
The host field name of the structure with the source text, the current locale object containing the text that you want to add and the current name of the selector needed to find the DOM element.
function getText(key, object, name, startIndex) { var elementKey=0; if(startIndex) elementKey = startIndex; for ( ; elementKey < document.getElementsByClassName(name + "-" + key).length; elementKey++) if (!isNaN(elementKey)) document.getElementsByClassName(name + "-" + key)[elementKey].textContent = object[key]; }
Processing the array of strings with source text is also performed by a separate function.
function getArrayText(key, object, name,startIndex)
The signature and the body of this function is no different from the past, except that elements from the array are assigned to the DOM elements.
function getArrayText(key, object, name, startIndex) { var elementKey=0; if(startIndex) elementKey = startIndex; for ( ; elementKey < document.getElementsByClassName(name + "-" + key).length; elementKey++) if (!isNaN(elementKey)) document.getElementsByClassName(name + "-" + key)[elementKey].textContent = object[key][elementKey % object[key].length]; }
The main recursive replacement function of the text is engaged in the classification of the current locale field into one of the 4 types listed above and the corresponding reaction to the resulting type:
function changeText(name, object, startIndex) { for (key in object) if (Array.isArray(object[key]) && typeof object[key] != 'string' && typeof object[key][0] == 'string') getArrayText(key, object, name); else if (typeof object[key] == "object" ){ if(isNaN(key)) changeText(name + "-" + key, object[key]); else changeText(name, object[key],key); } else getText(key, object, name, startIndex); }
This function accepts the current language locale and the root selector (in this case “locale”) as input. Further, when a nested structure or array of structures is detected, the function will recursively call itself, changing the input parameters accordingly.
The main advantage of the alternative approach is that the service described above does not require any functional changes, and is added as a js file using the locale file you created.
Conclusion
The essence of the approach described above consists in a fixed rule for describing selectors and building a locale file. Because of this, there is a unique opportunity to translate any pages out of the box and reuse the already translated material.
The algorithm for building selectors described above is not mandatory and critical for the operation of the service. The service is flexible for extending and adding new methods and algorithms, as well as for building selector names and the json locale structure. A possible advantage will be saving the locale in the browser’s cookie and changing the locale, depending on the location of the service user.
The source files of the sample site with automatic translation can be downloaded
on github .