Setting up a Javascript MVC / Adobe AIR 1.5 framework with multilingual support

Posted by Jacques Langlois

I have spent the last two months working with Adobe AIR. During this time, I have stumbled upon many obstacles and few online solutions to set me on the DRY path.

Unlike most javascript Air applications out there, Stories Matter contains a lot of pages and different functionalities. I did not want to use frames, cookies or the database to maintain the state of the application, so working with only one html file with various hidden divs required a lot of hard coding, was rather time consuming and led me to arcane page switching methods.

My biggest hurdle was the complete lack of a javascript debugger for Adobe Air. Can you imagine writing an application using only console.log to debug? However, this week, my wish was granted as Aptana released its beta version of the Adobe Air 1.5 JS debugger. With it, I was able to completely integrate JavascriptMVC (JSMVC) in AIR, using cached templates.

Here is how it’s done (note most obscure references are linked).

Download the latest version of JavascriptMVC 1.5

You’re going to have to take some time to read the wiki to understand how JavascriptMVC works. Using the scaffold tool makes things pretty easy though, even for a beginner, especially if you’ve worked with RoR or its PHP equilvanent, cakePHP. Download the latest version of JSMVC here.

Configure the Adobe AIR XML file

Once you’ve deployed the source code, you’re going to have to configure your AIR xml file because we’re going to place certain files away from their default AIR locations. The main html file is index.html and is located in the root folder. If you don’t know how to setup an AIR application, here’s a great tutorial.

<!-- The main SWF or HTML file of the application. Required. -->

	<content>index.html</content>

I’ve put all my resources in the ‘resources folder’. Here I’m specifying that AIR the application icons are in “resources/images/icons”

<!-- The icon the system uses for the application. For at least one resolution,
specify the path to a PNG file included in the AIR package. Optional. -->
<icon>
	<image16x16>resources/images/icons/sm_16.png</image16x16>
	<image32x32>resources/images/icons/sm_32.png</image32x32>
	<image48x48>resources/images/icons/sm_48.png</image48x48>
	<image128x128>resources/images/icons/sm_128.png</image128x128>
</icon>

Everything else in the xml file is pretty much straightfoward.

Create the index.html file

Thanks to JSMVC, we can keep the contents of the html file to a bare minimum:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
	<head>
		<title local_innerHTML="default.title" ></title>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		<link rel="stylesheet" href="resources/css/application.css" type="text/css" />
	</head>
	<body>
		<div id="background"></div>
		<div id="loading"> </div>
		<div id="content"></div>
		<div id="spaces"></div>
		<script type="text/javascript" src="jmvc/include.js?StoriesMatter,development"></script>
	</body>
</html>

Notice the title has a local_innerHTML=”default.title” tag. This is used for localisation purposes (more on that later).

The other interesting line is where JSMVC starts the engine:

<script type="text/javascript" src="jmvc/include.js?StoriesMatter,development"></script>

This runs your application file (usually /apps/applicationName.js), in this case, apps/StoriesMatter.js .

The application file

Note that declaration order is important:

//no conflict mode
MVC.no_conflict();
//AIR
include.resources('js/AIR/AIRLocalizer');
include.resources('js/AIR/AIRAliases');
include.resources('js/AIR/AIRIntrospector');
include.resources('js/AIR/AIRSourceViewer');
//MOOTOOLS
include.resources('js/mootools/mootools');

//APP
include.resources('js/menus');
include.resources('js/locale');
//JSMVC core and JStORM
include.plugins('core', 'model/jsorm','model/jsorm/providers/air','model/jsorm/dialects/sqlite');
//Bootstrap
include.resources('js/bootstrap');
//Load models, controllers and views
include(function(){ //runs after prior includes are loaded
include.models('projects','clips','interviews','spaces','countries');
include.controllers('app','projects','clips','interviews','browser','spaces');

MVC.View.config( {cache: true} );
include.views('views/main/index','views/projects/index','views/projects/edit');});

The application file, line by line

First, to avoid any conflicts with Mootools and other external librairies you may want to use, we must run JSMVC in no conflict mode:

//no conflict mode
 MVC.no_conflict();

AIR librairies and mootools

We load the AIR libraries, followed by mootools version 1.1 ( version 1.2.1 still has some issues with AIR 1.5 ). Mootools and ExtJS are the only valid “JS framework” candidates to not extensively use the eval function, dreaded by Adobe AIR, so no JQuery.

//AIR
 include.resources('js/AIR/AIRLocalizer');
 include.resources('js/AIR/AIRAliases');
 include.resources('js/AIR/AIRIntrospector');
 include.resources('js/AIR/AIRSourceViewer');
//MOOTOOLS
 include.resources('js/mootools/mootools');

Menus and localisation

We then initialize the menus and localisation, as our files are not in their default AIR locations.

include.resources('js/menus');

I did not extensively develop the menu tree, but the most important aspect to deal with was localisation. After trying for quite some time, I came to the conclusion there is no way to support i18n using AIRMenuBuilder as everything is read once in a static XML file.

Here’s the menu creation skeleton code, picked up mostly in this tutorial :

function CreateMenus() {
if (air.NativeWindow.supportsMenu) {
	nativeWindow.menu = new air.NativeMenu();
	targetMenu = nativeWindow.menu;
}
if (air.NativeApplication.supportsMenu) {
	targetMenu = air.NativeApplication.nativeApplication.menu;
}
var fileMenu;
fileMenu = targetMenu.addItem(new air.NativeMenuItem(localizer.getString('default', 'mnuFile')));
fileMenu.submenu = new air.NativeMenu();
menuItem = fileMenu.submenu.addItem(new air.NativeMenuItem(localizer.getString('default', 'mnuNewProject')));
menuItem.addEventListener(air.Event.SELECT, NewProjectSM);
menuItem = fileMenu.submenu.addItem(new air.NativeMenuItem(localizer.getString('default', 'mnuQuit')));
menuItem.addEventListener(air.Event.SELECT, QuitSM);
fileMenu = targetMenu.addItem(new air.NativeMenuItem(localizer.getString('default', mnuLang')));
fileMenu.submenu = new air.NativeMenu();
 for (i = 0; i < supportedLanguages.length; i++)
   {
     var localeCode = supportedLanguages[i];
     var localeStringCode = localeCode.toString();
     var languageName = localizer.getString('default', 'lblCurrentLanguage', null, localeCode);

     menuItem = fileMenu.submenu.addItem(new air.NativeMenuItem(languageName));
	  switch(localeStringCode) {
		 default:
			 case 'en' :
				 menuItem.addEventListener(air.Event.SELECT, function (){changeLanguage("en")}, false);
			 break;

			 case 'fr' :
				 menuItem.addEventListener(air.Event.SELECT, function (){changeLanguage("fr")}, false);
			 break;
		 }
 	}
}

And here’s what happens when we switch languages:

function RefreshMenus(){
	nativeWindow.menu.items[0].label = localizer.getString('default', 'mnuFile');
	nativeWindow.menu.items[0].submenu.items[0].label = localizer.getString('default', 'mnuNewProject');
	nativeWindow.menu.items[0].submenu.items[1].label = localizer.getString('default', 'mnuQuit');
	nativeWindow.menu.items[1].label = localizer.getString('default', 'mnuLang');
}

All this takes us to the localizer, taken from the Adobe AIR localizer tutorial. Read it first and the following code should be self-explanatory after reading this tutorial :

/*
* Initializes the application by loading the primary language strings into the user interface.
*/
function InitLoc()
{
	// Localizes the rest of the user interface, based on the stored language preference (if there is one)
	var language = loadLanguagePrefs();
	document.getElementById('changeLanguageControl').value = language;
	changeLanguage(language);
}
/*
* Changes the primary language, based on the selection of the changeLanguageControl element.
* The call to localizer.update() updates the user interface with strings defined for the language.
* The calle to saveLanguagePreferences saves the preference in a settings file.
*/
function changeLanguage(selectedLocale)
{
	var chain = air.Localizer.sortLanguagesByPreference(supportedLanguages, [selectedLocale]);
	localizer.setLocaleChain(chain);
	localizer.update();
	RefreshMenus();
	saveLanguagePref(selectedLocale);
}

JStORM Model plugins and bootstrap

So let’s head back to our application file. Our next step is to include the core JSMVC files as well as the JStORM plugin with the Adobe AIR provider and the SQLite dialect:

include.plugins('core', 'model/jsorm','model/jsorm/providers/air','model/jsorm/dialects/sqlite');

Once that is done, we call bootstrap to fire things up:

 include.resources('js/bootstrap');

Bootstrap contains initialization code and a lot of helper functions that don’t belong in the framework (notice I’ve put the DB file in the models folder). Here’s a snippet :

//definitions
const THUMBNAIL_SIZE = 100;

//global variables
// The Localizer object, defined in the AIRLocalizer.js file

var localizer = air.Localizer.localizer;
air.Localizer.ultimateFallbackLocale = "en";
localizer = air.Localizer.localizer;
localizer.setBundlesDirectory("resources/locale");
// An array of language codes, based on the locales defined for the application
var supportedLanguages = localizer.getLocaleChain().sort();
//JStORM Local connection
JStORM.connections = {
	"default": {
	PROVIDER: "AIR",
	DIALECT: "SQLite",
	PATH: "models/stories_matter.db"
	}
};

Loading the cached views

Back to our application file, we finally load our models, controllers and views.

//Load models, controllers and views
include(function(){ //runs after prior includes are loaded

 	include.models('projects','clips','interviews','spaces','countries');
 	include.controllers('app','projects','clips','interviews','browser','spaces');

   MVC.View.config( {cache: true} );

 	include.views('views/main/index','views/projects/index','views/projects/edit');
 });

In order to make templates work in Adobe Air (which stops any dynamic JS after the window.onload event), we must cache the views:

MVC.View.config( {cache: true} );

The main controller (appController)

Once our application file is created, JSMVC takes us by default to the main controller, and the load action is called when our window is ready. This is where we create our menus, stop the load bar and render our first view, ‘index’ from the Projects controller :

AppController = MVC.Controller.extend('main',{
	 load: function(params){
 			CreateMenus();
 			$('loading').style.display = "none";
 			ProjectsController.dispatch('index', params);
		 },
 });

The projects controller and index action (projectsController)

The ‘index’ action first renders the view, and then picks up every project we have in our database, using our JStORM model :

index: function(params){
	//load view
	projectData.project_id = 0;
        this.render({
	     action: "index",
	     to: "content"
	});
	//fill project list
	Projects.all().each(function(proj){
		var project = document.createElement("div");
		project.className = "project";
		project.setAttribute("project_id", proj.id);
		var project_img = document.createElement("img");
		project_img.onload = setWidthAndHeight;
		project_img.src = "file://" + proj.image_url;
		project.appendChild(project_img);

		var p = document.createElement("div");
		p.textContent = proj.name;
		project.appendChild(p);
		p = document.createElement("div");
		p.textContent = proj.short_description;
		project.appendChild(p);
		$('project_list').appendChild(project);
	});
},

An example model

Here’s the Projects model ( snippet ) :

var Projects = new JStORM.Model({
	name: "projects",

	fields: {

			id: new JStORM.Field({
				type: "Integer",
				isPrimaryKey:true
			}),
			name: new JStORM.Field({
				type: "String",
			}),
			short_description: new JStORM.Field({
				type: "String",
			}),
	connection: "default"
});

An example view

And here’s the Projects index View :

<div id="projects">
	<h1>Project Manager</h1>
	<a class="edit" href="#" ><h2>Create a new project</h2></a>
	<h2>Open an existing project</h2>
	<div id="project_list" class="list"></div>
</div>

As usual, the source-code can be downloaded here : http://code.google.com/p/storiesmatter/source/browse/#svn/trunk

Many thanks to Justin Meyer of JavascriptMVC who helped me along the way.

In closing, I hope this has been helpful for someone out there.

This entry was posted in Code and tagged , , , , , , , , . Bookmark the permalink.

One Response to Setting up a Javascript MVC / Adobe AIR 1.5 framework with multilingual support

  1. nate_h says:

    I’m wondering why you don’t think jQuery would work with AIR… I’m just about finished developing an AIR app created with jQuery.

Leave a Reply