# Building a Multi-language website

# Server-side vs Client-side localization

The standarized way to translate a web application is building the translatin mechanism on the server-side, and retrieve the translation files via AJAX calls. Altough, in some applications there are dynamically created messages which require a client-side mechanism for translation. The best way is to keep a mixed approach.

The next example has been built with the i18n Vue plugin for the client-side and a custom approach for the server-side translation, in order to make it easy to understand. The server-side approach should be changed depending on the project scope and properties.

# Multi-language client-side website with Vue.js

The i18n internationalization plugin is the most extended Vue plugin to add support for different languages to a website.

# Installation and usage

The installation is a simple process like other NPM modules

npm install vue-i18n
1

After installing, add the following code in the main.js file of the Vue App

import VueI18n from 'vue-i18n'

Vue.use(VueI18n);

const i18n = new VueI18n({
  locale: '<default-locale-ISO2-format>',
  messages: {
    'en': {
        // ...
    },
    'es':{ 
        //... 
    }, 
    // ...
  }
});

new Vue({
    //...
    i18n
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# Features

# Supports different message formats (names, lists, HTML, custom...)

const messages = {
  en: {
    message: {
      hello: '{msg} world'
    }
  }
}
1
2
3
4
5
6
7
<p>{{ $t('message.hello', { msg: 'hello' }) }}</p>
1

# Allows pluralization

const messages = {
  en: {
    apple: 'no apples | one apple | {count} apples'
  }
}
1
2
3
4
5
<p>{{ $tc('apple', 0) }}</p>
<p>{{ $tc('apple', 1) }}</p>
<p>{{ $tc('apple', 10, { count: 10 }) }}</p>
1
2
3

Note that you will need to use $tc() instead of $t()

# Language fallback

const i18n = new VueI18n({
    // ...
    fallbackLocale: 'en',
    silentTranslationWarn: true
});
1
2
3
4
5

The silentTranslationWarn property set to true is necessary to avoid console warnings when fallback

# Other features

  • DateTime formats
  • Locale message syntax (i.e. error list)
  • Vue v-t syntax for better performance (Instead of $t())
  • Hot reloading to increase performance

# Image Uploader App Features

  • Separated file for defining languages and messages
  • English fallback
  • Removed fallback warnings
  • Four language support (English, Spanish, French, Dutch)
  • $t() syntax (Easy to change to v-t)

# Multi-language server-side website with Node.js + Express.js

In order to show a solid example of how multi-language web apps can work, an example of server multi-language communication has been created.

# Preparing the client for receiving the data

It is mandatory to make a modification in the client-side for the routes that will have to request content to the server, as it is necessary to request the content if the user changes the language while browsing a page that needs to be translated. This breaks the concept of the Single Page Application, but there is the only way to do it if the pages are served one by one for each language. It is not a big problem since the page reload only has to be done when the user changes the language, and even can be restricted to only the ones that can receive translated content.

To get server-side data translated, it is also necessary that the request from the client-side includes information about in which language is preferred to get the requested content. There are two main ways to do it, and can be applied separately or mixed, and even adding some other properties and features to, for example, guess the best language to serve in first place instead of using one by default (fallback).

# 1. Serving content using different routes for each supported language: https://mysite.com/en/about

This is a common way to serve different content and with tools like Node.js and Express.js, it can be applied easily. The problem with this method arises when a user tries to share a link with other user who speaks a different language. In this case the receiver will have to change the language manually using the toggler or via URL. Other mechanisms can be applied in this situations to automatically change the route if the default receiver's browser language is different than the one of the URL.

# 2. Serving content using Headers

This is the easiest way to serve content in a proper way in different languages, since it does not require any plugin or third party module to build it, and also allows to maintain the existing routes of the application API. The problem of this method is that opens a door to XSS or CSRF attacks, but if the security of the application is well constructed, it should not be a problem. This method is usually applied together with the mentioned above to serve the first content in the most appropiated language for the requester.

# Other possibilities: Serving all the content all time and let the client-side manage it

This method is even easier than the previous one, but the problem is that if the server receives lots of requests and have to send large amounts of information for each one, this method can cause slowdowns in response times.

# Preparing the server-side

For this example, the client has been modified to send preferred the language in the header of each request. In order to prepare the server to send the content in the correct language, a localization folder has been created and different JSON files inside it, one for each supported language.

Success

The language files has been loaded to the server and then used to serve its content based on the request headers. There are also different plugins (like i18n-node (opens new window)) to create, store and manage language files.

The code to charge and manage the files in the server is the following:

const defaultLanguage = 'en';

const en = require('./languages/en.json');
const es = require('./languages/es.json');
const nl = require('./languages/nl.json');
const fr = require('./languages/fr.json');

const locales = { "en": en, "es": es, "nl": nl, "fr": fr };
1
2
3
4
5
6
7
8

Then this locales variable is used in each request to serve the content

app.get('/posts', function(req, res) {
    // Set the language to English if the header is not defined
    var lang = req.headers["accept-language"] || 'en';
    // If the header is defined but the language is 
    // not supported, return also the English content
    var responseData = locales[lang] || locales[defaultLanguage];
    res.json(responseData);
});
1
2
3
4
5
6
7
8

# General result applying mixed techniques

Translation System diagram

# Multi-language database design

# Column approach

Simplest and easiest way to proceed. This method consists of adding a new column to the table for each column that has to be translated.

Pros Cons
Simple Hard to maintain, works well for 2 or 3 languages
Easy queries Hard to add new languages, since new columns have to be created for each table that requires it
No duplicates Store empty space, if there are elements that don't need to be translated

# Multi-row approach

Similar to the column approach, but adding a new row for each element that has to be translated. Better than the previous one in case the same table has more than 1-2 columns to translate.

Pros Cons
Simple Hard to maintain, since every change to columns, even if the column changed does not require translation, has to be performed in all rows for each language
Easy queries Hard to add new languages
Duplicated content

# Single translation table approach

This solution is the cleanest one from database structure perspective. All texts that need to be translated are stored in a Single translation table.

Success

Pros Cons
Clean Complex querying, multiple JOINS required
Easy to add new languages Hard to maintain from the querying perspective. The more fields to translate, the more complex the query
Maintainable (all translations in one place) All translations in one place, if one table is missing, this will cause global problems for the application

# Additional translation table approach

Variation of the Single translation table approach, in order to make it easier to maintain and work with. For each table that stores information that may need to be translated, an additional table is created. The original table stores only language insensitive data and the new one all translated info

Success

Pros Cons
Clean May double the amount of tables
Easy to add new languages
Columns keep their names
Easy queries, one JOIN required

# Problems

  1. The app title (browser tab) cannot be translated since the Vue App is inside the HTML file that contains the Title
  2. Layout - Some languages like Arabic are right-to-left.

# Possible Solutions

  1. (?) Workaround to switch the title when app changes (Need more research)
  2. It would be necessary to have separate style files to properly handle the site appearance so that the text read and displayed correctly