This tutorial expands on the knowledge built in the App Anatomy  tutorial – although not a prerequisite of this tutorial, it’s a good place to start. The tutorial is in two parts: In Part 1, we'll build a basic client side language picker. In Part 2, we introduce the cloud to this.

A Basic Client-Side Language Picker

Introduction

In this tutorial we will see how to add multilingual support for our application. The basic concept is to create a HTML file to structure the application in /client, which will later be injected with language. For this tutorial, we won’t consider /cloud or /shared - all logic will be client side, on the device.

Changes in /client

In /client we build a tab application, much alike that presented in App Anatomy . Firstly, we want to include the scripts for our language utilities and defined languages in client/index.html. We will build these language files later.

 
<!-- Language scripts -->
<script type="text/javascript" src="/staticjs/utils_lang.js"></script>
<script type="text/javascript" src="/staticjs/lang/en_gb.js"></script>
<script type="text/javascript" src="/staticjs/lang/en_ie.js"></script>
<script type="text/javascript" src="/staticjs/lang/en_us.js"></script>
<script type="text/javascript" src="/staticjs/lang/fr.js"></script>

The HTML Template

When building our HTML application template, we do not place any text content inside our HTML tags. Instead, we build the structure of the template with HTML tags, and attach unique identifiers that represent the content they will contain. Language content will be injected at a later point. It’s important that the identifiers are unique, so a good practice would be to prefix all language identifiers with the name of your app. An example of a code snippet showing a welcome paragraph follows:

 
<div>
   <h1 id="”app_heading”"><a name=""></a></h1>
   <p id="”app_introduction”"></p>
</div>

Defining Language Files

Now we will build our language definitions. A language file name takes the format language_localisation.js - for example, en_gb.js This file contains a javascript variable of the same name. The variable is a javascript object which has a bunch of id and string value pairs. The ID should match the ID of the HTML tag we defined in index.html, where we want the language to appear. It's easiest to see this in practice - here is a small subset of /client/js/lang/en_gb.js:

 var en_gb = {
   // Welcome strings
   'app_heading': 'Welcome',
   'app_introduction': 'This tutorial shows how we can easily create a multi-lingual application.'
 }

In a similar manner, for translated content we create further files such as fr.js, de.js etc.

When writing translated content, it is recommended that special characters (á ä, etc) are encoded using their HTML entity representation (á ä etc). See: http://www.w3schools.com/tags/ref_entities.asp

Building our Language Utilities

Now we've set up a template ready to accept language content, we're going to build some functions in our utility file utils_lang.js. First, we'll declare a global called 'lang' which will store all language strings we need. We'll also declare the names of our language files, and and a default language.

 var lang;
 var languages = ["en_gb", "en_us", "en_ie", "fr"];
 var def = "en_gb"; // default language

For our language utility to function we now just need two more functions. One to build the global variable for language, which we will call buildLanguages(), and another to inject the required language into our HTML document, called updateLanguage().

For build languages, our approach will be to create a new 'object' for our return. We're going to iterate over the array of language file names we just built. Each element of this array is also the name of a global variable which we've already included in index.html. We've got a whole bunch of language based global variables - en_gb, en_ie, etc. We're now going to introduce a less known feature of the javascript language, a way of accessing variable names from a string value. The understanding of this concept is unimportant - it just means we can reference our language definitions from the array we made above. To do this, we look up the 'this' keyword, for example this["en_gb"]. Lastly, we append this object to our return object.

 function buildAllLanguages() { // Takes the language files & builds them into an object
   var ret = new Object();
   for (var i = 0; i < languages.length; i++) {
     var clIndex = languages[i]; // Current language index: Language identifier string, e.g. "en_gb"
     var clValue = this[clIndex]; // Current language value. Look for a global by the name clIndex, e.g. this["en_gb"]
     ret[clIndex] = clValue; // Append this to our return object with the clIndex as the array key, e.g. ret[en_gb] = clValue;
   }
   return ret;
 }

Now, we've built our language global - the hard part! The next function will update our language throughout the document. The approach is reasonably simple - first iterate over every 'key' in our desired language definition. Secondly, get a reference to the dom node required by looking up that ID, and changing it's HTML value to be the key value from our language definition. Let's briefly recap - if we've got a line in our language file like so:

 'app_heading': 'Hello',

The "app_heading" part is our key, and the "Hello" part is our value. So, we'll do:

 document.getElementById("app_heading").innerHTML = "Hello";

The whole function is as follows:

 function updateLanguage(l) {
   var newLanguage = lang[l]; // Look up the required language in our global
   for (id in newLanguage) { // Iterate over key and value pairs
     if (newLanguage.hasOwnProperty(id)) {
       $("#" + id).html(newLanguage[id]);
     }
   }
 }

Lastly, we're going to define a helper function so that our utility can be initialized by one simple function call.

 function initLanguages() {
   lang = buildAllLanguages(); // Set the global variable to our built languages
   updateLanguage(def); // Update the language for the first time to our default
 }

In the tutorial file, which you can clone from the developer studio, there is also some code for rendering a simple language picker so you can experiment with the end result. Although the explanation of this is simple, it is outside of the scope of this tutorial and can be ignored if not required.

Initializing Language Support

Now that we've built our language utilities, let's call the helper function to initialize internationalisation support. To do this, we need to modify our /client/init.js file. Inside our init() function, which is called when the app first loads, we place the following call:

 initLanguages();

Language Picker

We can add an optional language picker to our HTML template – the language options available to use will be injected into the <select> form tag.

 

That's it - we now have an app which supports language definitions across a whole range of languages.

Introducing the Cloud (Part 2)

Introduction

Now that we've built our language picker, let's make use of some of the cloud based features of the Feedhenry platform we learnt about in earlier tutorials.

As discussed previously, the directory structure within an app consists of three main folders. We've already seen /client, now we'll re-introduce cloud and shared.

  • /client
  • /shared - Where we'll define the language files, the global array, the default langauge and our buildAllLanguages() funciton
  • /cloud

Changes Required

/shared

First, we're going to move our language files folder ("currently client/default/js/lang") to "shared/lang". This makes our language files available to the cloud. We're also going to move our buildAllLanguages function to a new file in "/shared called utils_lang_shared.js".

We're not going to remove our files completely from index.html - we might need them in case of a fallback, as we will see later. Instead, change index.html to point to the new location in /shared. Our new language related includes in index.html looks like this:

 

Let's also move our languages array and our default language to shared - create a file called config.js. Since these are configurable options that may change, this makes sense.

 var languages = ["en_gb", "en_us", "en_ie", "fr"];
 var def = "en_gb"; // default language

/cloud

Remember from the App Anatomy tutorial that functions declared outside main.js have private scope. This means we can't call the shared action "buildAllLanguages" from "utils_lang_shared.js" as a cloud action. We need to create a small wrapper function to access buildAllLanguages() in main.js, that looks like this:

 function buildLanguages() {
   // Wrapper function - the scope of utils_lang_shared functions is private
   return buildAllLanguages();
 }

We have now made our language definitions available to both the client (device) and the cloud, and created a cloud action called buildLanguages() (our wrapper function). All that's left is to alter our code in /client to make use of this new action call.

/client

As before, our utility function is simply called from init.js as initLanguages(), and can be pictured as a black box function. The only change required to our utils_lang.js is the initLanguages function. The basic concept:

  • 1) We call our cloud action.
    • 2) If this can't be reached, we try our local datastore.
      • 3) If we can't reach the cloud, and we have no local languages in the datastore, we build them locally. (Worst case scenario - means the user has started the app for the first time without any internet access)

Let's build this in code. To do this, we're going to use a combination of a cloud action ($fh.act), a retrieval from local storage ($fh.data), or a fallback function if the last two fail (buildAllLanguages() in utils_lang.js).

To understand this code snippet, read the comments - Try 1, Try 2 and Try 3 have been marked and correspond to the above bullet points.

 function initLanguages() {
   // Try 1. Get from cloud
   $fh.act({
     act: 'buildLanguages'
   }, function(result) {
     lang = result;
     $fh.data({
       act: 'save',
       key: 'lang',
       val: JSON.stringify(lang)
     });
     updateLanguage(def);
   }, function(code, errorprops, params) {
     // Try 2. Failed to get lang from server. Probably offline - grab from our local datastore.
     $fh.data({
       key: 'lang'
     }, function(res) {
       // Check if we got back stored data
       if (null === res.val) {
         // Doesn't exist in local datastore. 
         // Try 3: Build it locally.
         lang = buildAllLanguages(); // Set the global variable to our built languages
         updateLanguage(def); // Update the language for the first time to our default
         $fh.data({
           act: 'save',
           key: 'lang',
           val: JSON.stringify(lang)
         });
       } else {
         // Successfully retrieved from local storage, let's use this.
         lang = JSON.parse(res.val);
         updateLanguage(def);
         // Store this in local keystore
       }
     }, function(error) {
       // An error with the datastore.
       // Try 3: Build it locally. 
       lang = buildAllLanguages(); // Set the global variable to our built languages
       updateLanguage(def); // Update the language for the first time to our default
       $fh.data({
         act: 'save',
         key: 'lang',
         val: JSON.stringify(lang)
       });
     })
   });
 }

That's it! We've now adopted our language utilities to pull language definitions built from the cloud, and rely on local storage fallback.