Internationalization (i18n)

Up until this point, we haven't had to use JavaScript beyond the dojo.require's. That's about to change. But as you'll see, a small amount of JavaScript goes a long way ... when hooked to Dijit and Dojo.

We're going to make the tax form accessible to readers of both languages: English and Swedish Chef. But first here's the wrong way to do it: make two pages with the same controls, the same styles, and the same wiring, but with different text on each. Very bad. When one page changes the other needs to change too. To add more languages, the job becomes even worse.

Since only the text changes, it be nice to have one copy of the page with textual placeholders. Dojo i18n is perfect for that.

Bundles and Resources

If you've used i18n in other languages like Java, the concepts are pretty similar here. Your application users are partitioned into "locales" - a language and a location and/or dialect. The ISO defines standard identifiers for locales, so that en-us is English in the U.S. and en-au is English in Australia. In our example, we'll be using a functional locale called "sw-chef". All other locales will map to the default locale, which in our case is just plain ol' English.

A bundle is a directory named after a locale, and a Dojo i18n resource is a JavaScript file underneath. As is common in other i18n setups, all of our bundles go under the directory "nls". For our i18n setup, we'll use the resource name "taxform". Here's the way it'll look:

[inline:taxform5.png]

The taxform.js underneath nls is the default resource, which looks like this:

({
first: "First Name",
last: "Last Name",
email: "Email Address",
filingDate: "Filing Date",
grossincome: "Please Enter Your 2007 Gross Income",
deductibles: "Total Deductions",
netincome: "Taxable Income",
taxpaid: "Total Witholding",
refund: "Your Refund",
owed: "Amount You Owe",
campaign: "Would you like to contribute an extra $3 to the Presidential Campaign Fund?"
})

The names before the : are called keys and they are one-word abbreviations for each of the text pieces. The sw-chef resource sw-chef/taxform.js is similar:

({
first: "Furst Neme-a",
last: "Lest Neme-a ",
email: "Emeeel Eddress",
filingDate: "Feeling Dete-a",
grossincome: "Pleese-a Inter Yuoor 2007 Gruss Incume-a",
deductibles: "Tutel Dedoocshuns",
netincome: "Texeble-a Incume-a",
taxpaid: "Tutel Veethulding",
refund: "Yuoor Reffoond",
owed: "Emuoont Yuoo Oove-a",
campaign: "Vuoold yuoo leeke-a tu cuntreeboote-a un ixtra $3 tu zee Preseedentiel Cempeeegn Foond? Bork Bork Bork!"
})

Now we somehow need to pull these names into the right places on the form. For that, we'll build our own custom widget.

An i18n Field Label Widget

Widgets are a lot like macros. You're basically substituting one "fake" tag with a dojoType - called the widget class - for a set of "real" tags. To transform the fake tag to the real tag, you use a template. For example, suppose our goal is to get this tag:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<label for="first" id="first_label">First Name</label>

To construct the Dijit template, we insert place holders for the parts that will vary:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<label for="${fieldid}" id="${fieldid}_label"></label>

Then nestle it inside a dijit.Declaration tag. We can place this tag and template anywhere in our HTML file, in theory, but most people place them under the BODY tag.

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<span dojoType="dijit.Declaration" widgetClass="TaxformI18n"
      defaults="{ fieldid: 'none' }">

    <label for="${fieldid}" id="${fieldid}_label"></label>
</span>

The widgetClass parameter gives the name TaxformI18n to our widget class. This will match the dojoType in our actual widgets. The defaults parameter defines all the placeholders and their default values. Any placeholder you use in the template must have a default here.

Having declared this, now the tag:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<div dojoType="TaxformI18n" fieldid="first"></div>

Will be replaced with

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<label for="first" id="first_label"></label>

Well, that's helpful but we still need our translated text. That's where we'll call Dojo.

The Dojo i18n API

The fieldid in our widget template can be used as a key into our resource. Ah ha! So you've got a label tag connected to first, which will be filled in with the text tied to "first" ... e.g. "First Name" or "Furst Neme-a". Given that the key is in "fieldid", here's the JavaScript to get the translation:

var labelNode = dojo.byId(this.fieldid + "_label");
var taxFormBundle = dojo.i18n.getLocalization("taxformI18n","taxform");
labelNode.innerHTML = taxFormBundle[this.fieldid];

The third line puts the translated text into the tag marked "fieldid_label".

So taxform is the resource name, but what is taxformI18n? That's a package name, which we define outside of the widget:

// This is necessary to i18n routines look in this directory for the nls folder
dojo.registerModulePath("taxformI18n","/online-book/taxform");
dojo.requireLocalization("taxformI18n", "taxform");

These two statements say "my resources are in http://yourserver/online-book/taxform/nls".

Now for the wiring. The dojo.Declaration must have a template, but it can also contain JavaScript code. You might be tempted to just plunk it in there with a <script type="text/javascript"> but that won't work. Instead you use the type "dojo/connect" and the event "startup." like this:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<span dojoType="dijit.Declaration" widgetClass="TaxformI18n"
    defaults="{ fieldid: 'none' }">

    <label for="${fieldid}" id="${fieldid}_label"></label>
       
    <script type='dojo/connect' event='startup'>
        var labelNode = dojo.byId(this.fieldid + "_label");
        var taxFormBundle = dojo.i18n.getLocalization("taxformI18n","taxform");
        labelNode.innerHTML = taxFormBundle[this.fieldid];
    </script>
</span>

And we're ready to go!

i18n in Action

So here's the complete listing:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
            "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Googolica Tax Form</title>
<style type="text/css">
        @import "http://o.aolcdn.com/dojo/1.0.0/dijit/themes/tundra/tundra.css";
        @import "http://o.aolcdn.com/dojo/1.0.0/dojo/resources/dojo.css"
</style>
<script type="text/javascript" src="/dojoroot/dojo/dojo.js"
        djConfig="parseOnLoad: true">
</script>
<script type="text/javascript">
        dojo.require("dojo.parser");
        dojo.require("dijit.form.TextBox");
        dojo.require("dijit.form.CheckBox");
        dojo.require("dijit.form.DateTextBox");
        dojo.require("dijit.form.CurrencyTextBox");
        dojo.require("dijit.Declaration");
        dojo.require("dijit.TitlePane");
        dojo.require("dijit.Tooltip");
               
        // This is necessary to i18n routines look in this directory for the nls folder
        dojo.registerModulePath("taxformI18n","/online-book/taxform");
        dojo.requireLocalization("taxformI18n", "taxform");
    </script>
</head>
<body class="tundra">
<span dojoType="dijit.Declaration" widgetClass="TaxformI18n"
        defaults="{ fieldid: 'none' }">

        <label for="${fieldid}" id="${fieldid}_label"></label>
       
        <script type='dojo/connect'     event='startup'>
        var labelNode = dojo.byId(this.fieldid + "_label");
        var taxFormBundle = dojo.i18n.getLocalization("taxformI18n","taxform");
        labelNode.innerHTML = taxFormBundle[this.fieldid];
    </script>
</span>
<form>
<div dojoType="TaxformI18n" fieldid="first"></div>
<input type="text" size="20" name="first" dojoType="dijit.form.TextBox"
        trim="true" propercase="true" />
<br>
<div dojoType="TaxformI18n" fieldid="last"></div>
<input type="text" size="20" name="last" dojoType="dijit.form.TextBox"
        trim="true" propercase="true" />
<br>
<span dojoType="TaxformI18n" fieldid="email"></span>: <input type="text"
        length="20" name="email" dojoType="dijit.form.TextBox" lowercase="true" />

<div dojoType="TaxformI18n" fieldid="filingDate"></div>
<input type="text" length="10" id="filingDate" name="filingDate"
        dojoType="dijit.form.DateTextBox">

<hr />
<ol>
        <li>
        <div dojoType="TaxformI18n" fieldid="grossincome"></div>
        <input type="text" maxlength="12" id="grossincome" name="grossincome"
                value="0" dojoType="dijit.form.CurrencyTextBox" required="true"
                currency="USD" />
<img src="symbol_help.gif" id="helpIncome" />
        <div dojoType="dijit.Tooltip" connectId="helpIncome">That's how
        much <b>money</b> you make.</div>
        </li>
        <li>
        <div dojoType="TaxformI18n" fieldid="deductibles"></div>
        <input type="text" dojoType="dijit.form.CurrencyTextBox"
                id="deductibles" name="deductibles" value="0" required="false"
                currency="USD" />
</li>
        <li>
        <div dojoType="TaxformI18n" fieldid="netincome"></div>
        <input type="text" value="0" dojoType="dijit.form.CurrencyTextBox"
                required="false" id="netincome" name="netincome" currency="USD" />
</li>
        <li>
        <div dojoType="TaxformI18n" fieldid="taxpaid"></div>
        <input type="text" value="0" class="fillwidth currency"
                dojoType="dijit.form.CurrencyTextBox" required="false" readonly="true"
                id="taxpaid" name="taxpaid" currency="USD" />
</li>
        <li>
        <div dojoType="TaxformI18n" fieldid="refund"></div>
        <input type="text" value="0" dojoType="dijit.form.CurrencyTextBox"
                required="false" readonly="true" id="refund" name="refund"
                currency="USD" />
</li>
        <div dojoType="TaxformI18n" fieldid="owed"></div>
        <input type="text" value="0" dojoType="dijit.form.CurrencyTextBox"
                required="false" readonly="true" id="owed" name="owed" currency="USD" />

        </li>
        <li>
        <div dojoType="TaxformI18n" fieldid="campaign"></div>
        <input type="checkbox" name="campaign" value="Y"
                dojoType="dijit.form.CheckBox">
</li>
</ol>
<div dojoType="dijit.TitlePane" open="false"
        title="Directions (click to Expand)" style="width:400px;height:300px">

Proin risus. Nullam rhoncus purus id turpis. Praesent aliquam adipiscing
ligula. Aenean lorem ante, accumsan quis, elementum id, cursus eu,
lorem. Fusce viverra. Ut tempor nisi at ipsum. Etiam sed nibh.</div>
</body>
</html>

The browser knows which locale to use based on the web browser installation, and it feeds this directly to Dojo, by default. So unless your locale is "sw-chef", your page will look exactly like the previous examples. But you can easily override this by changing the configuration when loading dojo.js:

And run the result.

[inline:taxform6.png]

It makes you want to throw kitchen utensils in the air with glee! OK, one more design run, and our form will be finished.