13 min read

(For more resources related to this topic, see here.)

Getting on with it

Before we define our model, let’s define a namespace where it will live. This is an important habit to establish since it relieves us of having to worry about whether or not we’ll collide with another function, object, or variable of the same name.

While there are various methods used to create a namespace, we’re going to do it simply using the following code snippet:

// quizQuestion.js var QQ = QQ || {};

Now that our namespace is defined, we can create our question object as follows:

QQ.Question = function ( theQuestion ) { var self = this;

Note the use of self: this will allow us to refer to the object using self rather than using this. (Javascript’s this is a bit nuts, so it’s always better to refer to a variable that we know will always refer to the object.)

Next, we’ll set up the properties based on the diagram we created from step two using the following code snippet:

self.question = theQuestion; self.answers = Array(); self.correctAnswer = -1;

We’ve set the self.correctAnswer value to -1 to indicate that, at the moment, any answer provided by the player is considered correct. This means you can ask questions where all of the answers are right.

Our next step is to define the methods or interactions the object will have. Let’s start with determining if an answer is correct. In the following code, we will take an incoming answer and compare it to the self.correctAnswer value. If it matches, or if the self.correctAnswer value is -1, we’ll indicate that the answer is correct:

self.testAnswer = function( theAnswerGiven ) { if ((theAnswerGiven == self.correctAnswer) || (self.correctAnswer == -1)) { return true; } else { return false; } }

We’re going to need a way to access a specific answer, so we’ll define the answerAtIndex function as follows:

self.answerAtIndex = function ( theIndex ) { return self.answers[ theIndex ]; }

To be a well-defined model, we should always have a way of determining the number of items in the model as shown in the following code snippet:

self.answerCount = function () { return self.answers.length; }

Next, we need to define a method that allows an answer to be added to our object. Note that with the help of the return value, we return ourselves to permitting daisy-chaining in our code:

self.addAnswer = function( theAnswer ) { self.answers.push ( theAnswer ); return self; }

In theory we could display the answers to a question in the order they were given to the object. In practice, that would turn out to be a pretty boring game: the answers would always be in the same order, and chances would be pretty good that the first answer would be the correct answer. So let’s give ourselves a randomized list using the following code snippet:

self.getRandomizedAnswers = function () { var randomizedArray = Array(); var theRandomNumber; var theNumberExists; // go through each item in the answers array for (var i=0; i<self.answers.length; i++) { // always do this at least once do { // generate a random number less than the // count of answers theRandomNumber = Math.floor ( Math.random() * self.answers.length ); theNumberExists = false; // check to see if it is already in the array for (var j=0; j<randomizedArray.length; j++) { if (randomizedArray[j] == theRandomNumber) { theNumberExists = true; } } // If it exists, we repeat the loop. } while ( theNumberExists ); // We have a random number that is unique in the // array; add it to it. randomizedArray.push ( theRandomNumber ); } return randomizedArray; }

The randomized list is just an array of numbers that indexes into the answers[] array. To get the actual answer, we’ll have to use the answerAtIndex() method.

Our model still needs a way to set the correct answer. Again, notice the return value in the following code snippet permitting us to daisy-chain later on:

self.setCorrectAnswer = function ( theIndex ) { self.correctAnswer = theIndex; return self; }

Now that we’ve properly set the correct answer, what if we need to ask the object what the correct answer is? For this let’s define a getCorrectAnswer function using the following code snippet:

self.getCorrectAnswer = function () { return self.correctAnswer; }

Of course, our object also needs to return the question given to it whenever it was created; this can be done using the following code snippet:

self.getQuestion = function() { return self.question; } }

That’s it for the question object. Next we’ll create the container that will hold all of our questions using the following code line:

QQ.questions = Array();

We could go the regular object-oriented approach and make the container an object as well, but in this game we have only one list of questions, so it’s easier to do it this way.

Next, we need to have the ability to add a question to the container, this can be done using the following code snippet:

QQ.addQuestion = function (theQuestion) { QQ.questions.push ( theQuestion ); }

Like any good data model, we need to know how many questions we have; we can know this using the following code snippet:

QQ.count = function () { return QQ.questions.length; }

Finally, we need to be able to get a random question out of the list so that we can show it to the player; this can be done using the following code snippet:

QQ.getRandomQuestion = function () { var theQuestion = Math.floor (Math.random() * QQ.count()); return QQ.questions[theQuestion]; }

Our data model is officially complete. Let’s define some questions using the following code snippet:

// quizQuestions.js // // QUESTION 1 // QQ.addQuestion ( new QQ.Question ( "WHAT_IS_THE_COLOR_OF_THE_SUN?" ) .addAnswer( "YELLOW" ) .addAnswer( "WHITE" ) .addAnswer( "GREEN" ) .setCorrectAnswer ( 0 ) );

Notice how we attach the addAnswer and setCorrectAnswer methods to the new question object. This is what is meant by daisy-chaining: it helps us write just a little bit less code.

You may be wondering why we’re using upper-case text for the questions and answers. This is due to how we’ll localize the text, which is next:

PKLOC.addTranslation ( "en", "WHAT_IS_THE_COLOR_OF_THE_SUN?", "What is the color of the Sun?" ); PKLOC.addTranslation ( "en", "YELLOW", "Yellow" ); PKLOC.addTranslation ( "en", "WHITE", "White" ); PKLOC.addTranslation ( "en", "GREEN", "Green" ); PKLOC.addTranslation ( "es", "WHAT_IS_THE_COLOR_OF_THE_SUN?", "¿Cuál es el color del Sol?" ); PKLOC.addTranslation ( "es", "YELLOW", "Amarillo" ); PKLOC.addTranslation ( "es", "WHITE", "Blanco" ); PKLOC.addTranslation ( "es", "GREEN", "Verde" );

The questions and answers themselves serve as keys to the actual translation. This serves two purposes: it makes the keys obvious in our code, so we know that the text will be replaced later on, and should we forget to include a translation for one of the keys, it’ll show up in uppercase letters.

PKLOC as used in the earlier code snippet is the namespace we’re using for our localization library. It’s defined in www/framework/localization.js. The addTranslation method is a method that adds a translation to a specific locale. The first parameter is the locale for which we’re defining the translation, the second parameter is the key, and the third parameter is the translated text.

The PKLOC.addTranslation function looks like the following code snippet:

PKLOC.addTranslation = function (locale, key, value) { if (PKLOC.localizedText[locale]) { PKLOC.localizedText[locale][key] = value; } else { PKLOC.localizedText[locale] = {}; PKLOC.localizedText[locale][key] = value; } }

The addTranslation method first checks to see if an array is defined under the PKLOC.localizedText array for the desired locale. If it is there, it just adds the key/value pair. If it isn’t, it creates the array first and then adds the key/value pair. You may be wondering how the PKLOC.localizedText array gets defined in the first place. The answer is that it is defined when the script is loaded, a little higher in the file:

PKLOC.localizedText = {};

Continue adding questions in this fashion until you’ve created all the questions you want. The quizQuestions.js file contains ten questions. You could, of course, add as many as you want.

What did we do?

In this task, we created our data model and created some data for the model. We also showed how translations are added to each locale.

What else do I need to know?

Before we move on to the next task, let’s cover a little more of the localization library we’ll be using. Our localization efforts are split into two parts: translation and data formatting .

For the translation effort , we’re using our own simple translation framework, literally just an array of keys and values based on locale. Whenever code asks for the translation for a key, we’ll look it up in the array and return whatever translation we find, if any. But first, we need to determine the actual locale of the player, using the following code snippet:

// www/framework/localization.js PKLOC.currentUserLocale = ""; PKLOC.getUserLocale = function() {

Determining the locale isn’t hard, but neither is it as easy as you would initially think. There is a property (navigator.language) under WebKit browsers that is technically supposed to return the locale, but it has a bug under Android, so we have to use the userAgent. For WP7, we have to use one of three properties to determine the value.

Because that takes some work, we’ll check to see if we’ve defined it before; if we have, we’ll return that value instead:

if (PKLOC.currentUserLocale) { return PKLOC.currentUserLocale; }

Next, we determine the current device we’re on by using the device object provided by Cordova. We’ll check for it first, and if it doesn’t exist, we’ll assume we can access it using one of the four properties attached to the navigator object using the following code snippet:

var currentPlatform = "unknown"; if (typeof device != 'undefined') { currentPlatform = device.platform; }

We’ll also provide a suitable default locale if we can’t determine the user’s locale at all as seen in the following code snippet:

var userLocale = "en-US";

Next, we handle parsing the user agent if we’re on an Android platform. The following code is heavily inspired by an answer given online at http://stackoverflow.com/a/7728507/741043.

if (currentPlatform == "Android") { var userAgent = navigator.userAgent; var tempLocale = userAgent.match(/Android.*([a-zA-Z]{2}-[a-zA-Z] {2})/); if (tempLocale) { userLocale = tempLocale[1]; } }

If we’re on any other platform, we’ll use the navigator object to retrieve the locale as follows:

else { userLocale = navigator.language || navigator.browserLanguage || navigator.systemLanguage || navigator.userLanguage; }

Once we have the locale, we return it as follows:

PKLOC.currentUserLocale = userLocale; return PKLOC.currentUserLocale; }

This method is called over and over by all of our translation codes, which means it needs to be efficient. This is why we’ve defined the PKLOC.currentUserLocale property. Once it is set, the preceding code won’t try to calculate it out again. This also introduces another benefit: we can easily test our translation code by overwriting this property. While it is always important to test that the code properly localizes when the device is set to a specific language and region, it often takes considerable time to switch between these settings. Having the ability to set the specific locale helps us save time in the initial testing by bypassing the time it takes to switch device settings. It also permits us to focus on a specific locale, especially when testing.

Translation of text is accomplished by a convenience function named __T() . The convenience functions are going to be our only functions outside of any specific namespace simply because we are aiming for easy-to-type and easy-to-remember names that aren’t arduous to add to our code. This is especially important since they’ll wrap every string, number, date, or percentage in our code.

The __T() function depends on two functions: substituteVariables and lookupTranslation. The first function is de fined as follows:

PKLOC.substituteVariables = function ( theString, theParms ) { var currentValue = theString; // handle replacement variables if (theParms) { for (var i=1; i<=theParms.length; i++) { currentValue = currentValue.replace("%" + i, theParms[i-1]); } } return currentValue; }

All this function does is handle the substitution variables. This means we can define a translation with %1 in the text and we will be able to replace %1 with some value passed into the function.

The next function, lookupTranslation, is defined as follows:

PKLOC.lookupTranslation = function ( key, theLocale ) { var userLocale = theLocale || PKLOC.getUserLocale(); if ( PKLOC.localizedText[userLocale] ) { if ( PKLOC.localizedText[userLocale][key.toUpperCase()] ) { return PKLOC.localizedText[userLocale][key.toUpperCase()]; } } return null; }

Essentially, we’re checking to see if a specific translation exists for the given key and locale. If it does, we’ll return the translation, but if it doesn’t, we’ll return null. Note that the key is always converted to uppercase, so case doesn’t matter when looking up a translation.

Our __T() function looks as follows:

function __T(key, parms, locale) { var userLocale = locale || PKLOC.getUserLocale(); var currentValue = "";

First, we determine if the translation requested can be found in the locale, whatever that may be. Note that it can be passed in, therefore overriding the current locale. This can be done using the following code snippet:

if (! (currentValue=PKLOC.lookupTranslation(key, userLocale)) ) {

Locales are often of the form xx-YY, where xx is a two-character language code and YY is a two-character character code. My locale is defined as en-US. Another player’s might be defined as es-ES.

If you recall, we defined our translations only for the language. This presents a problem: the preceding code will not return any translation unless we defined the translation for the language and the country.

Sometimes it is critical to define a translation specific to a language and a country. While various regions may speak the same language from a technical perspective, idioms often differ. If you use an idiom in your translation, you’ll need to localize them to the specific region that uses them, or you could generate potential confusion.

Therefore, we chop off the country code, and try again as follows:

userLocale = userLocale.substr(0,2); if (! (currentValue=PKLOC.lookupTranslation(key, userLocale)) ) {

But we’ve only defined translations for English (en) and Spanish(es)! What if the player’s locale is fr-FR (French)? The preceding code will fail, because we’ve not defined any translation for the fr language (French). Therefore, we’ll check for a suitable default, which we’ve defined to be en-US, American English:

userLocale = "en-US"; if (! (currentValue=PKLOC.lookupTranslation(key, userLocale)) ) {

Of course, we are now in the same boat as before: there are no translations defined for en-US in our game. So we need to fall back to en as follows:

userLocale = "en"; if (! (currentValue=PKLOC.lookupTranslation(key, userLocale)) ) {

But what happens if we can’t find a translation at all? We could be mean and throw a nasty error, and perhaps you might want to do exactly that, but in our example, we’re just returning the incoming key. If the convention of capitalizing the key is always followed, we’ll still be able to see that something hasn’t been translated.

currentValue = key; } } } }

Finally, we pass the currentValue parameter to the substituteVariables property in order to process any substitutions that we might need as follows:

return PKLOC.substituteVariables( currentValue, parms ); }

Summary

In this article we saw the file quizQuestion.js which was the actual model: it specified how the data should be formatted and how we can interact with it. We also saw the quizQuestions.js file, which contained our actual question data.

Resources for Article :


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here