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.
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.
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>
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>
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
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.
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();
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.
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.
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
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.
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:
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.