神刀安全网

Building a Full Featured ES6 Translation Service

One way to increase users engagement is to translate all the texts in your web application to their language. Building the ability to translate texts and change languages is easy. In this article I’ll show you how to build a translation service that supports new languages fetching, string interpolation and pluralization. In this article I’ll use ES2015 (ES6) and at the end I’ll provide the complete code along with working git repository, including unit tests.

We will build our translation module step by step. To be able to translate texts, our translation module has to get a language name and a translation object as an initial input. The translation object is a JSON object that contains key-value pairs of words and phrases, and it supports nesting. An example to such translation object is:

Spanish translation object

{     "title": "el título de mi web",     "pages": {         "page1": {             "name": "nombre"             "age": "años"         },         "page2": {             "search": "buscar",             "loading": "cargando"         }     } }

Translate a Key

The first ability to implement in our translation module is a key translation. That is, a function that gets a key and returns the translated text (according to the translation object). Let’s initiate the translation module and write this method:

TranslationService module initialized

class TranslationService {     // Constructor that get the default language and translation object     constructor(language, translationObject) {         this.currentLanguage = language;         this.translationsDictionary = {             [language]: translationObject         };     }      // A method that gets a key and translates it according the translation object     translate(key) {         var tokens = key.split('.');         var value = this.translationsDictionary[this.currentLanguage];         for (let i = 0; i < tokens.length && value !== undefined; i++) {             value = value[tokens[i]];         }          if (value === undefined) {             value = '';         }          return value;     } }  export default TranslationService;

The translate(key) gets a key (for example pages.page1.age ) and walks through the translation object to find the value of that key. That is, for the previous translation object, translate('pages.page1.age') will return the string años .

Notice that the service maintains the current language ( currentLanguage ) and a dictionary of languages and their translation objects ( translationsDictionary ).

Set New Translation Language

Now lets allow the user to change the application’s language. We do this by providing setLanguage(language, translationObject) method that updates the current language and set the new translation object in the translations dictionary.

Set language method

    ...     ...     setLanguage(language, translationObject) {         if (!translationObject) {             translationObject = this.translationsDictionary[language];         }         // Both language and translationObject required         if (language && this.currentLanguage !== language && translationObject) {             this.currentLanguage = language;             this.translationsDictionary[language] = translationObject;         }     }     ...     ...

Notice that both language and translationObject are required ( translationObject can also be acquired from translationsDictionary in case it was set previously).

Listen to Changes

An important point related to setting the language is listening to changes. In case the user changes the language, all the application translated texts should be change also into the new selected language. In other words, our service has to expose an ability to listen to language changes (change callback), so the developer will be able to re-translate relevant texts upon change. Let’s implement an onChange() method that gets a callback as a parameter, which will be invoked upon language change.

Handle change callbacks

class TranslationService {     constructor(....) {         ...         ...         this.changeHandlers = [];     }     ...     ...     onChange(callback) {         if (callback && typeof callback === 'function') {             this.changeHandlers.push(callback);         }     }     ...     ...     setLanguage(language, translationObject) {         if (!translationObject) {             translationObject = this.translationsDictionary[language];         }         // Both language and translationObject required         if (language && this.currentLanguage !== language && translationObject) {             this.currentLanguage = language;             this.translationsDictionary[language] = translationObject;             this.changeHandlers.forEach((callback) => callback());         }     }     ...     ...

In addition to onChange() implementation, TranslationService initiates changeHandlers attribute and invoke all change callback in setLanguage() .

The next step is to teach our service to load translation objects by itself (in case a translation object for the requested language doesn’t exist).

Fetching Translation Objects

give a man a fish and you feed him for a day; teach a man to fish and you feed him for a lifetime Since setLanguage(language, translationObject) might be called from a different places in our application, we don’t always have a simple ability to provide a translation object. Therefore, instead of providing a translation object each time the user changes the application language, we will add the ability to provide a function that will fetch translation object upon constructing TranslationService instance:

Provide translationFetcher method upon constructing `TranslationService` instance

    ...     ...     constructor(language, translationObject, translationFetcher = null) {         ...         ...         this.translationFetcher = translationFetcher;     }     ...     ...

translationFetcher() is a function that gets a language and returns a promise, which will be resolved with the translation object of that language. An example for such translationFetcher function can be:

translationFetcher example

function translationFetcherExample(language) {     var url = `http://www.webdeveasy.com/translations/${language}`;     return new Promise(function(resolve, reject) {         var xhr = new XMLHttpRequest();         xhr.open('GET', url);          req.onload = function() {             if (req.status == 200) {                 try {                     let translationObject = JSON.parse(req.responseText);                     resolve(translationObject);                 } catch(e) {                     let error = new Error(`Parse Error: ${e.toString()}`);                     reject(error);                 }             } else {                 let error = new Error(req.statusText);                 reject(error);             }         };          req.onerror = function() {             var error = new Error('Network Error');             reject(error);         };          req.send();     }); }

If you are not familiar with JavaScript Promises, this article about JavaScript Promises will give you a solid knowledge.

Now, in case translationObject wasn’t supplied to setLanguage() , the translationFetcher() method should be called. Let’s integrate translationFetcher() with setLanguage() :

Integrate setLanguage() with translationFetcher()

    ...     ...     setLanguage(language, translationObject = null) {         if (!translationObject) {             translationObject = this.translationsDictionary[language];         }         return new Promise((resolve, reject) => {             // Now, only language is required             if (language) {                 if (this.currentLanguage === language) {                     resolve();                 } else if (translationObject) {                     this.applyLanguage(language, translationObject);                     resolve();                 } else {                     this.resolveTranslationObject(language)                         .then(                             (translationObject) => {                                 this.applyLanguage(language, translationObject);                                 resolve();                             },                             (reason) => reject(reason)                         );                 }             } else {                 reject('setLanguage: language is mandatory');             }         });     }     applyLanguage(language, translationObject) {         this.currentLanguage = language;         this.translationsDictionary[language] = translationObject;         this.changeHandlers.forEach((callback) => callback());     }     resolveTranslationObject(language) {         return new Promise((resolve, reject) => {             if (this.translationFetcher && typeof this.translationFetcher === `function`) {                 var promise = this.translationFetcher(language);                 if (!promise || typeof promise.then !== 'function') {                     reject('translationFetcher should return a promise');                 }                  promise.then(                         (translationObject) => {                             if (translationObject) {                                 resolve(translationObject);                             } else {                                 reject('translationFetcher resolved without translation object');                             }                         },                         (reason) => reject(`translationFetcher failed: ${reason}`)                     );             } else {                 reject('Cannot resolve translation object');             }         });     }     ...     ...

In case setLanguage() didn’t get translationObject , it will call the supplied translationFetcher (using resolveTranslationObject() method helper). The promise returned from the translation fetcher should resolved with the new translation object. This translationObject is used to set up the new language in the translations dictionary and apply the new language ( applyLanguage() ).

The current implementation of setLanguage() is asynchronic. In case translationObject wasn’t supplied, translationFetcher() will be executed and it might take some time to fetch the new translation object. In order to let the user know when the new language applied, setLanguage() now returns a promise that resolved or rejected upon success or failure.

That is our very basic translation service. It is very small and has no extra-features but it is fully operational. Now you can translate texts using translation objects, change languages, listen to changes and let the service load translation objects by itself when required.

However, I would like to add two important abilities to our translation service: string interpolation and pluralization support. Without those features, it will be very difficult to use our translation service.

String Interpolation

The current implementation of our translation service has many problems when dealing with dynamic values. For example, assume we have a phrase that contains the name of the current user (which is a dynamic value and changes for each user): “Hello Dan”, where “Dan” is the current user name. Our English translation object will be:

en.json

{     "welcomeMessage": "Hello " }

Translate welcome message to English

var translation = new TranslationService('en', en); var username = 'Dan'; var welcomeMessage = translation.translate('welcomeMessage') + username;

However, in Spanish, the desired phrase is “Dan, hola!” and the Spanish translation object looks like:

es.json

{     "welcomeMessage": ", hola!" }

Translate welcome message to Spanish

var translation = new TranslationService('es', es); var username = 'Dan'; var welcomeMessage = username + translation.translate('welcomeMessage');

As you can see, in English, the user name should be appended to the end of the phrase, where in Spanish, the user name should be prepended to the beginning of the phrase.Such cases force us to add another layer of logic related to the translation to our code, which is ugly and hard to maintain. The solution is to interpolate the parameters inside the strings. In other words, the phrases should contain placeholders for the dynamic values. The phrases for the example presented above should be:

en.json

{     "welcomeMessage": "Hello {{username}}" }

es.json

{     "welcomeMessage": "{{username}}, hola!" }

{{username}} is the placeholder for the user name and the translate() method should get this value and interpolate it inside the phrase.

Let’s add string interpolation support to the translate() method:

Add string interpolation support to the translate() method

    ...     ...     translate(key, interpolation = undefined) {         var tokens = key.split('.');         var value = this.translationsDictionary[this.currentLanguage];         for (let i = 0; i < tokens.length && value !== undefined; i++) {             value = value[tokens[i]];         }          if (value === undefined) {             value = '';         }          // Handle interpolation         if (interpolation) {             value = value.replace(/{{(/w*)}}/gi, function(m, param) {                 let match = '';                 if (interpolation.hasOwnProperty(param)) {                     match = interpolation[param];                 }                 return match;             });         }          return value;     }     ...     ...

Now translate() gets interpolation object as a parameter and searches for placeholders in the translated key. The placeholders are defined by words wrapped in two curly braces and those words are the parameters names. For each parameter found, we look for it’s value in the interpolation object and in case there is one, we replace the placeholder with it.

Using the string interpolation feature is very simple, here are some more examples of interpolations in translation object:

Interpolations in translation object

{     "welcomeMessage": "Hello {{username}}",     "viewMoreItems": "View {{itemsCount}} more items",     "title": "@{{username}} has sent you a message in {{chatRoomName}}",     "user": {         "points": "You have {{points}} points"     } }

Translate with interpolation

var welcomeMessage = translationService.translate('welcomeMessage', { username: 'NaorYe' }); var viewMoreItems = translationService.translate('viewMoreItems', { itemsCount: '32' }); var title = translationService.translate('welcomeMessage', {     username: 'NaorYe',     chatRoomName: 'The Good Guys' }); var points = 59; var userPoints = translationService.translate('user.points', { points });

Pluralization

Another important feature for our translation service is pluralization support. Suppose we have a search form and we want to tell how many items found:

  • For zero items we’d like to write: “Nothing found!”
  • For one item we’d like to write: “Only one item found.”
  • For more then one items we’d like to write: “Many items found!”

With the current translation service, doing this requires three different translation phrases and our application has to select the right translation phrase using an if statement that checks a value of variable:

Translation object without pluralization support

{     "itemsCountZero": "Nothing found!",     "itemsCountOne": "Only one item found.",     "itemsCountMany": "Many items found!" }

Translate items count without pluralization support

var itemsCount = ...; var translationKey = null; if (itemsCount === 0) {     translationKey = 'itemsCountZero'; } else if itemsCount === 1) {     translationKey = 'itemsCountOne'; } else { // itemsCount > 1     translationKey = 'itemsCountMany'; } var translatedPhrase = translationService.translate(translationKey);

Instead, I’d like to add this functionality into the translation service and supply the items count to the translation service so it will be able to determine itself the correct phrase:

Translation object with pluralization support

{     "itemsCount": {         "zero": "Nothing found!",         "1": "One item found!",         "2": "Two items found!",         "many": "Many items found!"     } }

Translate items count with pluralization support

var itemsCount = ...; var interpolation = ...; var translatedPhrase = translationService.translate('itemsCount', interpolation, itemsCount);

This will keep our code separated from the translation process and therefore much cleaner. Let’s add pluralization support to the translate() method:

Add pluralization support to the translate() method

    ...     ...     translate(key, interpolation = undefined, pluralValue = undefined) {         var tokens = key.split('.');         var value = this.translationsDictionary[this.currentLanguage];         for (let i = 0; i < tokens.length && value !== undefined; i++) {             value = value[tokens[i]];         }          if (value === undefined) {             value = '';         }          // Handle pluralization         if (typeof value === 'object') {             if (typeof pluralValue === 'number') {                 let pluralization = value;                 // If pluralValue holds the number `X`, check whether `X` is a key in pluralization.                 // If it is, use the phrase of `X`. Otherwise, use `zero` or `many`.                 if (pluralization.hasOwnProperty(pluralValue)) {                     value = pluralization[pluralValue];                 } else {                     if (pluralValue === 0) {                         value = pluralization.zero;                     } else { // pluralValue is a number and not equals to 0, therefore pluralValue > 0                         value = pluralization.many;                     }                 }             } else {                 value = '';             }         }          // Handle interpolation         if (interpolation) {             value = value.replace(/{{(/w*)}}/gi, function(m, param) {                 let match = '';                 if (interpolation.hasOwnProperty(param)) {                     match = interpolation[param];                 }                 return match;             });         }          return value;     }     ...     ...

I’ve added pluralValue number parameter to the translate() method. When provided, translate() decides which plural phrase to use according to pluralValue value. Notice that the translation key has to lead to an object that hold the pluralization strings.

That’s all! Now we have a translation service that supports languages fetching, string interpolation and pluralization.

Complete Code and Usage Examples

Here is the full code of the service:

TranslationService module

class TranslationService {     constructor(language, translationObject, translationFetcher = null) {         this.currentLanguage = language;         this.translationsDictionary = {             [language]: translationObject         };         this.translationFetcher = translationFetcher;         this.changeHandlers = [];     }      onChange(callback) {         if (callback && typeof callback === 'function') {             this.changeHandlers.push(callback);         }     }      setLanguage(language, translationObject = null) {         if (!translationObject) {             translationObject = this.translationsDictionary[language];         }         return new Promise((resolve, reject) => {             // Now, only language is required             if (language) {                 if (this.currentLanguage === language) {                     resolve();                 } else if (translationObject) {                     this.applyLanguage(language, translationObject);                     resolve();                 } else {                     this.resolveTranslationObject(language)                         .then(                             (translationObject) => {                                 this.applyLanguage(language, translationObject);                                 resolve();                             },                             (reason) => reject(reason)                         );                 }             } else {                 reject('setLanguage: language is mandatory');             }         });     }      applyLanguage(language, translationObject) {         this.currentLanguage = language;         this.translationsDictionary[language] = translationObject;         this.changeHandlers.forEach((callback) => callback());     }      resolveTranslationObject(language) {         return new Promise((resolve, reject) => {             if (this.translationFetcher && typeof this.translationFetcher === `function`) {                 var promise = this.translationFetcher(language);                 if (!promise || typeof promise.then !== 'function') {                     reject('translationFetcher should return a promise');                 }                  promise.then(                         (translationObject) => {                             if (translationObject) {                                 resolve(translationObject);                             } else {                                 reject('translationFetcher resolved without translation object');                             }                         },                         (reason) => reject(`translationFetcher failed: ${reason}`)                     );             } else {                 reject('Cannot resolve translation object');             }         });     }      translate(key, interpolation = undefined, pluralValue = undefined) {         var tokens = key.split('.');         var value = this.translationsDictionary[this.currentLanguage];         for (let i = 0; i < tokens.length && value !== undefined; i++) {             value = value[tokens[i]];         }          if (value === undefined) {             value = '';         }          // Handle pluralization         if (typeof value === 'object') {             if (typeof pluralValue === 'number') {                 let pluralization = value;                 // If pluralValue holds the number `X`, check whether `X` is a key in pluralization.                 // If it is, use the phrase of `X`. Otherwise, use `zero` or `many`.                 if (pluralization.hasOwnProperty(pluralValue)) {                     value = pluralization[pluralValue];                 } else {                     if (pluralValue === 0) {                         value = pluralization.zero;                     } else { // pluralValue is a number and not equals to 0, therefore pluralValue > 0                         value = pluralization.many;                     }                 }             } else {                 value = '';             }         }          // Handle interpolation         if (interpolation) {             value = value.replace(/{{(/w*)}}/gi, function(m, param) {                 let match = '';                 if (interpolation.hasOwnProperty(param)) {                     match = interpolation[param];                 }                 return match;             });         }          return value;     } }  export default TranslationService;

Usage Example

Suppose our initial language is English. Let’s define an English translation object:

en.json – English translation object

{     "messages": {         "writtenBy": "Commenter: {{name}}",         "messagesCount": {             "zero": "",             "1": "1 Message",             "2": "Only 2 messages, let's add some noise to this chat!",             "48": "Wow, 48 messages! Keep talking!"             "many": "{{messagesCount}} Messages"         }     },     "share": {         "facebook": "Share on Facebook",         "twitter": "Share on Twitter",         "email": {             "send": "Send Email",             "subject": "@{{name}} has shared a topic with you",             "content": "Topic: {{topic}}"         }     } }

In order to use the service we have to create an instance of TranslationService that will be share to our application. Our initial language is English, enTranslationObject is the English translation object we just defined and translationFetcher is a translation fetcher method as:

translation.js

import TranslationService from 'translation-service'; import enTranslationObject from './en.json'; ... ...  function translationFetcher(language) {     return new Promise(function(resolve, reject) {         ...         ...         ...     }); }  var translationService = new TranslationService('en', enTranslationObject, translationFetcher);  export default translationService;

Now we are ready to translate our application. Just import translation.js and call setLanguage() and translate() when needed. Since the texts that should be translated are laying in DOM elements, I suggest creating a TranslatedText wrapper component, written for your favorite framework, so it would be easy to translate texts in your application. Such component should get translation key, interpolation object and a pluralValue as an input, and create an element (usually <span> ) that holds the translated value. It also should attach a listener for a language changes and re-render the element with the new translation upon change.

Translate examples

import translation from 'translation';  var text = translation.translate('messages.writtenBy', { name: 'Charles Dickens' }); console.log(`${text} is equal to "Commenter: Charles Dickens"`);  var messagesCount = 2; // Since we don't really know the value of messagesCount, we pass it to the translate method as an // interpolation object. The translated text might need it. text = translation.translate('messages.messagesCount', { messagesCount: messagesCount }, messagesCount); console.log(`${text} is equal to "Only 2 messages, let's add some noise to this chat!"`);  text = translation.translate('share.facebook'); console.log(`${text} is equal to "Share on Facebook"`);  text = translation.translate('share.email.subject', { name: 'NaorYe' }); console.log(`${text} is equal to @NaorYe has shared a topic with you"`);

I’ve added a GitHub repository for the translation service, including tests. Feel free to use it.

That’s all! Thanks for reading my article, I hope it was useful.

NaorYe

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Building a Full Featured ES6 Translation Service

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮