/* AIRLocalizer.js - Revision: 1.0 */
// Copyright 2007-2008. Adobe Systems Incorporated.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of Adobe Systems Incorporated nor the names of its
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
// Using the function method to obfuscate inner globals to the main script, so that the
// "window" remains untouched.
(function(){
/**
* This is automatically replaced while building
* @version "1.0"
* @private
*/
var version = "1.0";
/**
* Just to make sure we have the right shortcuts included
* @namespace window.air
* @private
*/
var air = {
File : window.runtime.flash.filesystem.File,
FileStream : window.runtime.flash.filesystem.FileStream,
FileMode : window.runtime.flash.filesystem.FileMode,
Capabilities : window.runtime.flash.system.Capabilities
};
/**
* Utility functions
* @namespace util
* @private
*/
var util = {
/**
* Returns the content of a file
* @param file air.File File that should be read
* @return string
*/
stringFromFile : function stringFromFile(file){
var fileStream = new air.FileStream();
fileStream.open(file, air.FileMode.READ);
var result = fileStream.readUTFBytes(file.size);
fileStream.close();
return result;
}
};
/**
* Used for sorting the locales array
* @private
*/
function LocaleId(){
this.lang = '';
this.script = '';
this.region = '';
this.extended_langs = [];
this.variants = [];
this.extensions = {};
this.privates = [];
this.privateLangs = false;
};
LocaleId.prototype = {
transformToParent: function(){
if(this.privates.length>0){
this.privates.splice(this.privates.length-1, 1);
return true;
}
var lastExtensionName = null;
for(var i in this.extensions){
if(this.extensions.hasOwnProperty(i)){
lastExtensionName = i;
}
}
if(lastExtensionName){
var lastExtension = this.extensions[lastExtensionName];
if(lastExtension.length==1){
delete this.extensions[ lastExtensionName ];
return true;
}
lastExtension.splice(lastExtension.length-1, 1);
return true;
}
if(this.variants.length>0){
this.variants.splice(this.variants.length-1, 1);
return true;
}
if(this.script!=''){
//check if we can surpress the script
if(LocaleId.getScriptByLang(this.lang)!=''){
this.script='';
return true;
}else if(this.region==''){
//maybe the default region can surpress the script
var region = LocaleId.getDefaultRegionForLangAndScript(this.lang, this.script);
if(region!=''){
this.region = region;
this.script = '';
return true;
}
}
}
if(this.region!=''){
if(!(this.script=='' && LocaleId.getScriptByLang(this.lang) == '')){
this.region='';
return true;
}
}
if(this.extended_langs.length>0){
this.extended_langs.splice(this.extended_langs.length-1, 1);
return true;
}
return false;
},
isSiblingOf: function(other){
if(this.lang==other.lang&&this.script==other.script){
return true;
}
return false;
},
equals: function(locale){
return this.toString() == locale.toString();
},
toString: function(){
var stack = [ this.lang ];
Array.prototype.push.apply(stack, this.extended_langs);
if(this.script!='') stack.push(this.script);
if(this.region!='') stack.push(this.region);
Array.prototype.push.apply(stack, this.variants);
for(var i in this.extensions){
if(this.extensions.hasOwnProperty(i)){
stack.push(i);
Array.prototype.push.apply(stack, this.extensions[i]);
}
}
if(this.privates.length>0){
stack.push('x');
Array.prototype.push.apply(stack, this.privates);
}
return stack.join('_');
},
canonicalize: function(){
for(var i in this.extensions){
if(this.extensions.hasOwnProperty(i)){
//also clear zero length extensions
if(this.extensions[i].length==0) delete this.extensions[i];
else this.extensions[i] = this.extensions[i].sort();
}
}
this.extended_langs = this.extended_langs.sort();
this.variants = this.variants.sort();
this.privates = this.privates.sort();
if(this.script == ''){
this.script = LocaleId.getScriptByLang(this.lang);
}
//still no script, check the region
if(this.script == '' && this.region!=''){
this.script = LocaleId.getScriptByLangAndRegion(this.lang, this.region);
}
if(this.region=='' && this.script!=''){
this.region = LocaleId.getDefaultRegionForLangAndScript(this.lang, this.script);
}
}
}
LocaleId.registry = {
"scripts": ["cyrl", "latn", "ethi", "arab", "beng", "cyrl", "thaa", "tibt", "grek", "gujr", "hebr", "deva", "armn", "jpan", "geor", "khmr", "knda", "kore", "laoo", "mlym", "mymr", "orya", "guru", "sinh", "taml", "telu", "thai", "nkoo", "blis", "hans", "hant", "mong", "syrc"],
"scriptById": {"cyrl": 5, "latn": 1, "ethi": 2, "arab": 3, "beng": 4, "thaa": 6, "tibt": 7, "grek": 8, "gujr": 9, "hebr": 10, "deva": 11, "armn": 12, "jpan": 13, "geor": 14, "khmr": 15, "knda": 16, "kore": 17, "laoo": 18, "mlym": 19, "mymr": 20, "orya": 21, "guru": 22, "sinh": 23, "taml": 24, "telu": 25, "thai": 26, "nkoo": 27, "blis": 28, "hans": 29, "hant": 30, "mong": 31, "syrc": 32} ,
"defaultRegionByLangAndScript": {"bg": {5: "bg"}, "ca": {1: "es"}, "zh": {30: "tw", 29: "cn"}, "cs": {1: "cz"}, "da": {1: "dk"}, "de": {1: "de"}, "el": {8: "gr"}, "en": {1: "us"}, "es": {1: "es"}, "fi": {1: "fi"}, "fr": {1: "fr"}, "he": {10: "il"}, "hu": {1: "hu"}, "is": {1: "is"}, "it": {1: "it"}, "ja": {13: "jp"}, "ko": {17: "kr"}, "nl": {1: "nl"}, "nb": {1: "no"}, "pl": {1: "pl"}, "pt": {1: "br"}, "ro": {1: "ro"}, "ru": {5: "ru"}, "hr": {1: "hr"}, "sk": {1: "sk"}, "sq": {1: "al"}, "sv": {1: "se"}, "th": {26: "th"}, "tr": {1: "tr"}, "ur": {3: "pk"}, "id": {1: "id"}, "uk": {5: "ua"}, "be": {5: "by"}, "sl": {1: "si"}, "et": {1: "ee"}, "lv": {1: "lv"}, "lt": {1: "lt"}, "fa": {3: "ir"}, "vi": {1: "vn"}, "hy": {12: "am"}, "az": {1: "az", 5: "az"}, "eu": {1: "es"}, "mk": {5: "mk"}, "af": {1: "za"}, "ka": {14: "ge"}, "fo": {1: "fo"}, "hi": {11: "in"}, "ms": {1: "my"}, "kk": {5: "kz"}, "ky": {5: "kg"}, "sw": {1: "ke"}, "uz": {1: "uz", 5: "uz"}, "tt": {5: "ru"}, "pa": {22: "in"}, "gu": {9: "in"}, "ta": {24: "in"}, "te": {25: "in"}, "kn": {16: "in"}, "mr": {11: "in"}, "sa": {11: "in"}, "mn": {5: "mn"}, "gl": {1: "es"}, "kok": {11: "in"}, "syr": {32: "sy"}, "dv": {6: "mv"}, "nn": {1: "no"}, "sr": {1: "cs", 5: "cs"}, "cy": {1: "gb"}, "mi": {1: "nz"}, "mt": {1: "mt"}, "quz": {1: "bo"}, "tn": {1: "za"}, "xh": {1: "za"}, "zu": {1: "za"}, "nso": {1: "za"}, "se": {1: "no"}, "smj": {1: "no"}, "sma": {1: "no"}, "sms": {1: "fi"}, "smn": {1: "fi"}, "bs": {1: "ba"}},
"scriptIdByLang": {"ab": 0, "af": 1, "am": 2, "ar": 3, "as": 4, "ay": 1, "be": 5, "bg": 5, "bn": 4, "bs": 1, "ca": 1, "ch": 1, "cs": 1, "cy": 1, "da": 1, "de": 1, "dv": 6, "dz": 7, "el": 8, "en": 1, "eo": 1, "es": 1, "et": 1, "eu": 1, "fa": 3, "fi": 1, "fj": 1, "fo": 1, "fr": 1, "frr": 1, "fy": 1, "ga": 1, "gl": 1, "gn": 1, "gu": 9, "gv": 1, "he": 10, "hi": 11, "hr": 1, "ht": 1, "hu": 1, "hy": 12, "id": 1, "in": 1, "is": 1, "it": 1, "iw": 10, "ja": 13, "ka": 14, "kk": 5, "kl": 1, "km": 15, "kn": 16, "ko": 17, "la": 1, "lb": 1, "ln": 1, "lo": 18, "lt": 1, "lv": 1, "mg": 1, "mh": 1, "mk": 5, "ml": 19, "mo": 1, "mr": 11, "ms": 1, "mt": 1, "my": 20, "na": 1, "nb": 1, "nd": 1, "ne": 11, "nl": 1, "nn": 1, "no": 1, "nr": 1, "ny": 1, "om": 1, "or": 21, "pa": 22, "pl": 1, "ps": 3, "pt": 1, "qu": 1, "rn": 1, "ro": 1, "ru": 5, "rw": 1, "sg": 1, "si": 23, "sk": 1, "sl": 1, "sm": 1, "so": 1, "sq": 1, "ss": 1, "st": 1, "sv": 1, "sw": 1, "ta": 24, "te": 25, "th": 26, "ti": 2, "tl": 1, "tn": 1, "to": 1, "tr": 1, "ts": 1, "uk": 5, "ur": 3, "ve": 1, "vi": 1, "wo": 1, "xh": 1, "yi": 10, "zu": 1, "cpe": 1, "dsb": 1, "frs": 1, "gsw": 1, "hsb": 1, "kok": 11, "mai": 11, "men": 1, "nds": 1, "niu": 1, "nqo": 27, "nso": 1, "son": 1, "tem": 1, "tkl": 1, "tmh": 1, "tpi": 1, "tvl": 1, "zbl": 28},
"scriptIdByLangAndRegion": {"zh": {"cn": 29, "sg": 29, "tw": 30, "hk": 30, "mo": 30}, "mn": {"cn": 31, "sg": 5}, "pa": {"pk": 3, "in": 22}, "ha": {"gh": 1, "ne": 1}}};
LocaleId.getScriptByLangAndRegion = function(lang, region){
var langRegions = LocaleId.registry.scriptIdByLangAndRegion[ lang ];
if(typeof langRegions=='undefined') return '';
var scriptId = langRegions[region];
if(typeof scriptId!='undefined'){
return LocaleId.registry.scripts[scriptId].toLowerCase();
}
return '';
}
LocaleId.getScriptByLang = function(lang){
var scriptId = LocaleId.registry.scriptIdByLang[ lang ];
if(typeof scriptId!='undefined'){
return LocaleId.registry.scripts[scriptId].toLowerCase();
}
return '';
}
LocaleId.getDefaultRegionForLangAndScript = function(lang, script){
var lang = LocaleId.registry.defaultRegionByLangAndScript[ lang ];
var scriptId = LocaleId.registry.scriptById[ script ];
if(typeof lang!='undefined' && typeof scriptId !='undefined' ){
return lang[scriptId] || "";
}
return '';
}
LocaleId.fromString = function(str){
//states
{
var PrimaryLanguage = 0,
ExtendedLanguages = 1,
Script = 2,
Region = 3,
Variants = 5,
Extensions = 6,
Privates = 7;
}
var localeId = new LocaleId();
var state = PrimaryLanguage;
var subtags = str.replace(/-/g, '_').split('_');
var last_extension;
for(var i=0, l=subtags.length; i<l ;i++){
var subtag = subtags[i].toLowerCase();
if(state==PrimaryLanguage){
if(subtag=='x'){
localeId.privateLangs = true; //not used in our implementation, but makes the tag private
}else if(subtag=='i'){
localeId.lang += 'i-'; //and wait the next subtag to complete the language name
}else{
localeId.lang += subtag;
state ++;
}
}else{
//looging for an extended language - 3 chars
// a script - 4 chars
// a region - 2-3 chars
// a variant - alpha with at least 5 chars or numeric with at least 4 chars
// an extension/private singleton - 1 char
var subtag_length = subtag.length; //store it for faster use later
if(subtag_length==0) continue; //skip zero-lengthed subtags
var firstChar = subtag[0].toLowerCase();
if(state<=ExtendedLanguages && subtag_length==3){
localeId.extended_langs.push(subtag);
if(localeId.extended_langs.length==3){ //allow a maximum of 3 extended langs
state = Script;
}
}else if ( state <= Script && subtag_length==4 ){
localeId.script = subtag;
state = Region;
}else if( state <= Region && (subtag_length==2 || subtag_length==3) ){
localeId.region = subtag;
state = Variants;
}else if ( state <= Variants &&
(
( firstChar>='a' && firstChar<='z' && subtag_length>=5 )
||
( firstChar>='0' && firstChar<='9' && subtag_length>=4 )
)
){
//variant
localeId.variants.push(subtag);
state = Variants;
}else if ( state < Privates && subtag_length==1 ){ //singleton
if(subtag == 'x'){
state = Privates;
last_extension = localeId.privates;
} else {
state = Extensions;
last_extension = localeId.extensions[subtag] || [];
localeId.extensions[subtag] = last_extension;
}
}else if(state >= Extensions){
last_extension.push(subtag);
}
}
}
localeId.canonicalize();
return localeId;
}
var LocaleSorter = {
/**
* Promote only that locales from preferenceLocales that have parents in locales
* @private
* @param locales String[] List of locales to be sorted.
* @param preferenceLocales String[] List of locales in the preference order
* @param addAll Adds all the locales at the end, even though no locale is in the preferences list. Default is true
* @return String[]
*/
sortLocalesUsingPreferences: function(_locales, _preferenceLocales, ultimateFallbackLocale, addAll){
var result = [];
var haveLocale = {};
function prepare(list){
var resultList = [];
for(var i=0,l=list.length;i<l;i++) {
resultList.push (list[i].toLowerCase().replace(/-/g,'_'));
}
return resultList;
}
var locales = prepare(_locales);
var preferenceLocales = prepare(_preferenceLocales);
if(ultimateFallbackLocale&&ultimateFallbackLocale!=''){
var ultimateFallbackLocale = ultimateFallbackLocale.toLowerCase().replace(/-/g,'_');
var found = false;
for(var i=0, l=preferenceLocales.length; i<l; i++){
if(preferenceLocales[i]==ultimateFallbackLocale){
found = true;
}
}
if(!found){
preferenceLocales.push(ultimateFallbackLocale);
}
}
for(var j=0, k=locales.length; j<k; j++){
haveLocale[ locales[j] ] = j;
}
function promote(locale){
if(typeof haveLocale[locale]!='undefined'){
result.push( _locales[ haveLocale[locale] ] );
delete haveLocale[locale];
}
}
for(var i=0, l=preferenceLocales.length; i<l; i++){
var plocale = LocaleId.fromString( preferenceLocales[i] );
// step 1: promote the perfect match
promote(preferenceLocales[i]);
promote(plocale.toString());
// step 2: promote the parent chain
while(plocale.transformToParent()){
promote(plocale.toString());
}
//parse it again
plocale = LocaleId.fromString( preferenceLocales[i] );
// step 3: promote the order of siblings from preferenceLocales
for(var j=0; j<l; j++){
var locale = preferenceLocales[j];
if( plocale.isSiblingOf ( LocaleId.fromString( locale ) ) ){
promote(locale);
}
}
// step 4: promote all remaining siblings (aka not in preferenceLocales)
for(var j=0, k=locales.length; j<k; j++){
var locale = locales[j];
if( plocale.isSiblingOf( LocaleId.fromString( locale ) ) ){
promote( locale );
}
}
}
if(addAll){
// check what locales are not already loaded and push them.
// using the "for" because we want to preserve order
for(var j=0, k=locales.length; j<k; j++){
promote(locales[j]);
}
}
return result;
},
};
/**
* LocalizerEvent
* @constructor
* @private
*/
function LocalizerEvent(name, config){
for(var prop in config){
this[prop] = config[prop];
}
this.name = name;
};
LocalizerEvent.prototype = {
toString: function(){
return "[LocalizerEvent: "+this.name+"]";
}
}
/**
* Parses the property files and generates a collection of keys and values
* @constructor
* @private
*/
function ResourceBundle(bundleFile){
this.keys = {};
try{
this.parse(util.stringFromFile(bundleFile));
this.found = true;
}catch(e){
// file not found
// die silently
this.found = false;
}
};
ResourceBundle.prototype = {
/**
* Parses the property file
* @param source Content of the property file
*/
parse: function(source){
var source = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/^(?:\s*?)(?:!|#)([^\n\r]*)$/gm, '');
var rg = /^(?:[ \t\f]*)((?:\\=|\\:|\\\\|[^=:\s])+)(?:[ \t\f]*)(?:$|(?:[=: \t\f](?:[ \t\f]*)((?:[^\n\r]*(?:\\(?:\r\n|\n|\r))+)*[^\n\r]*)$))/gm;
var reformat = /\\(?:\r\n|\n|\r)(?:\s*)/g;
var decoder = /\\(.)/g;
var decoderCodes = { ':' : ':', '=':'=', 'n': "\n", 'r':"\r", 't':"\t", "\\":"\\" };
var decoderFn = function(r, a){ return decoderCodes.hasOwnProperty(a) ? decoderCodes[a] : r }
var line;
while( line = rg.exec(source) ){
var nodeName = line[1].replace(/\uFEFF/g, '').replace(decoder, decoderFn);
var value = line[2] || "";
this.keys[nodeName] = value.replace(reformat, "").replace(decoder, decoderFn);
}
},
/**
* Returns the value of a resource
* @param key string The key name of the resource. This is case sensitvie
* @return string
*/
get: function(key){
return this.keys[key] || null;
}
};
/**
* Holds the content of a localized file
* @constructor
* @private
*/
function ResourceFile(bundleFile){
try{
this.content = util.stringFromFile(bundleFile);
this.found = true;
}catch(e){
// file not found
// die silently
this.content = null;
this.found = false;
}
};
ResourceFile.prototype = {
/**
* Returns the content of the file
* @return string
*/
getContent: function(){
return this.content;
}
};
/**
* Takes every node on the DOM and replaces attributes based on a prefix
* @param parent LocalizerPrivate The localizer object used to get the resource strings
* @constructor
* @private
*/
function DOMWalker(parent){
this.parent = parent;
}
DOMWalker.prototype = {
/**
* Chechs if an attribute is a local attribute
* @param attr DOMAttribute The attribute that should be checked
* @return bool
*/
isLocalAttribute: function(attr){
return attr.nodeName.toLowerCase().substr(0, this.attributePrefixLength)==this.attributePrefix;
},
/**
* Returns the attribute name that should be replace on a local attribute
* @param attr DOMAttribute The attribute that should be checked
* @return string
*/
getLocalAttribute: function(attr){
return attr.nodeName.toLowerCase().substr(this.attributePrefixLength);
},
/**
* Replaces attributes on a single node
* @private
*/
walkNode: function(node){
var attributePrefix = this.attributePrefix;
var attributeInnerPrefix = attributePrefix+'innerhtml';
if(!node.attributes) return;
var params = [];
var setInner = false;
var innerValue = null;
for(var i=node.attributes.length-1;i>=0;i--){
var attr = node.attributes[i];
if(attr.name.toLowerCase()==attributeInnerPrefix){
setInner = true;
innerValue = attr.value;
}else if(this.isLocalAttribute(attr)){
params.push([this.getLocalAttribute(attr), attr.value]);
}
}
if(setInner){
var value = this.getHtmlString(innerValue);
try{
node.innerHTML = value||innerValue;
}catch(e){
node.textContent = value||innerValue;
}
}
for(var i=params.length-1;i>=0;i--){
var param = params[i];
var value = this.getHtmlString(param[1]);
node.setAttribute(param[0], value||param[1]);
}
},
/**
* Replaces each node under 'node'
* @param node DOMElement
* @public
*/
run: function (node){
this.attributePrefix = this.parent.attributePrefix.toLowerCase();
this.attributePrefixLength = this.attributePrefix.length;
var treeWalker = document.createTreeWalker(
node||document,
NodeFilter.SHOW_ELEMENT,
{ acceptNode: function(node) { return NodeFilter.FILTER_ACCEPT; } },
false
);
while(treeWalker.nextNode()) this.walkNode(treeWalker.currentNode);
},
/**
* Separates the bundleName and resourceName and returns the string from locale chain
* @param resourceName String Default bundle is "default"
* @return String
*/
getHtmlString: function(resourceName){
var bundleName;
var i = resourceName.indexOf('.');
if(i!=-1){ bundleName = resourceName.substr(0, i); resourceName=resourceName.substr(i+1); }
else bundleName = "default"
return this.parent.getStringFromChain(bundleName, resourceName);
}
};
/**
* Creates a new object used in Localizer to store loaded bundles, locale chain and bundles path
* @private
* @constructor
* @v
*/
function LocalizerPrivate(parent){
this.parent = parent;
// dom walker needed to update nodes
this.domWalker = new DOMWalker(this);
// Locale chain is the internal order that the framework uses to search for locales
this.localeChain = [];
// When the developer sets the locale chain we should disable the automatic detection
this.autoLocaleChain = true;
// Loaded locales should be sorted for later use
this.localeCache = {};
// Default bundle path used to solve the locales directories
this.bundlePath = new air.File('app:/locale/');
// Store the system capabilities for later use
this.userPreference = air.Capabilities.languages || [air.Capabilities.language];
// Event listeners
this.eventListeners = {};
this.attributePrefix = 'local_';
}
LocalizerPrivate.prototype = {
/**
* Creates a new locale object that caches bundles and files
* registers the locale for later use
* @private
* @param locale The localeName to be created
* @return LocaleHash
*/
createLocaleCache: function(locale){
var obj = {
name : locale,
bundles: {},
files: {}
};
this.localeCache[locale] = obj;
return obj;
},
/**
* Returns the locale cache object. In case it is not already loaded it is created.
* @private
* @param locale String The localeName to be returned
* @return LocaleHash
*/
getLocaleCache: function(locale){
return this.localeCache[locale] || this.createLocaleCache(locale);
},
/**
* Removes all the cached locales that are not in the locale chain
* @private
*/
cleanupCache: function (){
var isInChain = {};
for(var i in this.localeChain) isInChain[ this.localeChain[i] ] = true;
for(var locale in this.localeCache ) {
if( ! isInChain[locale] ){
this.localeCache [ locale ] = null;
delete this.localeCache [locale];
}
}
},
/**
* Removes all the cached locales
* @private
*/
clearCache: function (){
for(var locale in this.localeCache ) {
this.localeCache [ locale ] = null;
delete this.localeCache [locale];
}
},
/**
* Creates the bundle object. Also caches it for later use
* @private
* @param locale String The locale name where it should search the bundle
* @param bundlename String The bundle name for the returned bundle
* @return ResourceBundle
*/
createBundle: function(locale, bundleName){
var localeCache = this.getLocaleCache(locale);
var file = this.bundlePath.resolvePath(locale).resolvePath(bundleName+".properties");
var resourceBundle = new ResourceBundle(file);
localeCache.bundles[bundleName] = resourceBundle;
if(!resourceBundle.found){
setTimeout(function(obj){
obj.dispatchEvent(new LocalizerEvent(Localizer.BUNDLE_NOT_FOUND, {
bundleName: bundleName,
locale: locale
}));
}, 0, this);
}
return resourceBundle;
},
/**
* Loads a bundle and returns it.
* @private
* @param locale String The locale name where it should search the bundle
* @param bundleName String The bundle name to be loaded
* @param useCache Bool Setting this to false will force the function to reread the bundle. Default is true
* @return ResourceBundle
*/
loadBundle: function(locale, bundleName, useCache){
var localeCache = this.getLocaleCache(locale);
useCache=(typeof useCache=='undefined')?true:useCache;
return (useCache&&localeCache.bundles[bundleName]) || this.createBundle(locale, bundleName);
},
/**
* Creates a new ResourceFile based on the fileName
* @private
* @param locale String The locale name where it should search the file
* @param fileName String Filename that should be loaded
* @return ResourceFile
*/
createFile: function(locale, fileName){
var localeCache = this.getLocaleCache(locale);
var file = this.bundlePath.resolvePath(locale).resolvePath(fileName);
var resourceFile = new ResourceFile(file);
localeCache.files[fileName] = resourceFile;
if(!resourceFile.found){
setTimeout(function(obj){
obj.dispatchEvent(new LocalizerEvent(Localizer.FILE_NOT_FOUND, {
fileName: fileName,
locale: locale
}));
}, 0, this);
}
return resourceFile;
},
/**
* Loads a file and returns it.
* @private
* @param locale String The locale name where it should search the file
* @param bundleName String The file name to be loaded
* @param useCache Bool Setting this to false will force the function to reread the file. Default is true
* @return ResourceFile
*/
loadFile: function(locale, fileName, useCache){
var localeCache = this.getLocaleCache(locale);
useCache=(typeof useCache=='undefined')?true:useCache;
return (useCache&&localeCache.files[fileName]) || this.createFile(locale, fileName);
},
/**
* Automatically discovers the available locales using the listing directory of the bundle path,
* returns an array of locales
* @private
* @return StringArray
*/
discoverAndReturnAvailableLocales: function (){
var folder = this.bundlePath;
// check if the file exists and is a directory
if(!(folder.exists&&folder.isDirectory)){
// just die sillently, because a higher level should take care of that
return [];
}
var localeChain = []
var localeFolders = folder.getDirectoryListing();
for(var i=0, l=localeFolders.length; i<l; i++){
var folder = localeFolders[i];
// checking if we got a real folder
if(folder.isDirectory){
localeChain.push(folder.name);
}
}
return localeChain;
},
/**
* Same as sortLocalesUsingPreferences, but this time uses the systems pref
* @param locales String[] List of locales to be sorted.
* @param addAll Adds all the locales at the end, even though no locale is in the preferences list. Default is true
* @return String[]
*/
sortLocalesUsingSystemPreferences: function(locales, addAll){
return LocaleSorter.sortLocalesUsingPreferences( locales, this.userPreference, Localizer.ultimateFallbackLocale, addAll)
},
/**
* Discovers locale chain the bundle path and sets the current locale chain.
* The function will fail silently if the locale chain has been set by the user
* @param fireEvent bool If true it will fire change events. Default is true
*/
autoDiscoverLocaleChain: function(fireEvent){
if(this.autoLocaleChain){
// set the new locale chain
var oldLocaleChain = this.localeChain;
this.localeChain = this.sortLocalesUsingSystemPreferences( this.discoverAndReturnAvailableLocales(), true);
// remove from cache not neeed locales
this.cleanupCache();
if(typeof fireEvent=='undefined') fireEvent = true;
if(fireEvent){
this.diffChainsAndFireEvent(this.localeChain, oldLocaleChain);
}
}
},
/**
* Diffs two chains and returns true if they are not equal
* @param newChain String[]
* @param oldChain String[]
* @return String[]
*/
diffChains: function(newChain, oldChain){
if(newChain.length!=oldChain.length) return true;
for(var i=0, l=newChain.length; i<l; i++){
if(newChain[i]!=oldChain[i])
return true;
}
return false;
},
/**
* Clones a chain list
* @param chain String[]
* @return String[]
*/
cloneChain: function(chain){
var result = [];
for(var i=0, l=chain.length; i<l; i++){
result.push( chain[i] );
}
return result;
},
/**
* diffs two locale chains and fires an event if changed
* @param newChain String[]
* @param oldChain String[]
* @private
*/
diffChainsAndFireEvent: function(newChain, oldChain){
if(this.diffChains(newChain, oldChain)){
this.dispatchEvent(new LocalizerEvent(Localizer.LOCALE_CHANGE, {
localeChain: this.cloneChain(newChain)
}));
}
},
/**
* Sets the locale chain and disables automatic locale chain detection
* @param chain String[]
*/
setLocaleChain: function ( chain ){
if(chain && chain.hasOwnProperty && chain.hasOwnProperty('length') && chain.length>0 ){
var oldChain = this.localeChain;
this.localeChain = this.cloneChain(chain);
// switch off auto locale chain detection
this.autoLocaleChain = false;
// send some events
this.diffChainsAndFireEvent(this.localeChain, oldChain);
}
},
/**
* Returns the current locale chain
* @return String[]
*/
getLocaleChain: function(){
return this.cloneChain(this.localeChain);
},
/**
* Returns the source string, but replaces the "{i}" string with args[i]
* @param source String
* @param args String[]
* @return String
*/
template: function(source, args){
var parser = /{([^}]*)}/g;
var a = source.split(parser);
var result = [];
var d=0;
for(var i=0,l=a.length;i<l;i++,d=1-d){
if(d){
if(args.hasOwnProperty(a[i]) && (args[a[i]]!=null) && (typeof args[a[i]]!='undefined')){
result.push(args[a[i]]);
}else{
result.push("{"+a[i]+"}");
}
}else{
result.push(a[i]);
}
}
return result.join('');
},
// typical add/remove dispatch listener object
addEventListener: function(eventName, handler){
var eventListeners = this.eventListeners;
if(!eventListeners[eventName]){
eventListeners[eventName] = [];
}else{
// prevent adding the same listener twice
this.removeEventListener(eventName, handler);
}
eventListeners[eventName].push( {
eventName:eventName,
handler:handler
});
},
removeEventListener: function(eventName, handler){
var handlers = this.eventListeners[eventName];
if(!handlers) return;
for(var i=handlers.length-1;i>=0;i--){
var evh = handlers[i];
if(evh.eventName==eventName&&evh.handler==handler){
handlers.splice(i, 1);
}
}
},
dispatchEvent: function(ev){
var handlers = this.eventListeners[ev.name];
if(!handlers) return;
for(var i=handlers.length-1;i>=0;i--){
handlers[i].handler.call(this.parent, ev);
}
},
/**
* Loads the bundle and returns the resource value
* @public
* @param bundleName String
* @param resourceName String
* @param locale String
* @return String
*/
getString: function(bundleName, resourceName, locale){
var bundle = this.loadBundle(locale, bundleName);
var result = bundle.get(resourceName);
if(result==null){
setTimeout(function(obj){
obj.dispatchEvent(new LocalizerEvent(Localizer.RESOURCE_NOT_FOUND, {
bundleName: bundleName,
resourceName: resourceName,
locale: locale
}));
}, 0, this);
}
return result;
},
/**
* loads the file and returns the contents
* @public
* @param fileName String
* @param locale String
* @return String
*/
getFile: function(fileName, locale){
var file = this.loadFile(locale, fileName);
return file.getContent();
},
/**
* Uses the locale chain to get the first defined value
* @public
* @param bundleName String
* @param resourcename String
* @return String
*/
getStringFromChain: function(bundleName, resourceName){
var result;
var chain = this.localeChain;
if(chain){
for(var i=0, l=chain.length; i<l; i++){
result = this.getString( bundleName, resourceName, chain[i] );
if(result!=null) return result;
}
}
return null;
},
/**
* Uses the locale chain to get the first defined file
* @public
* @param fileName
* @return String
*/
getFileFromChain: function(fileName){
var result;
var chain = this.localeChain;
if(chain){
for(var i=0, l=chain.length; i<l; i++){
result = this.getFile( fileName, chain[i] );
if(result!=null) return result;
}
}
return null;
},
/**
* Updates the dom
* @param domElement DOMElement Optional, default is document
* @public
*/
update: function(domElement){
this.domWalker.run( domElement || document );
}
};
// Localizer constructor, this should be callable by this script only. Use the canCreate trick in order
// to throw an error when it is not set to true.
// Also note that the canCreateLocalizer is visible just in this script
var canCreateLocalizer = false;
function Localizer(){
// Throw an error when the users wants to create the Localizer using the new keyword. This is a
// singleton and there's only one creator for it (air.Localizer.localizer getter)
if(!canCreateLocalizer){
throw new Error("Cannot create an air.Localizer instance using the 'new' keyword. Use air.Localizer.localizer instead.");
}
this._private = new LocalizerPrivate( this );
this._private.autoDiscoverLocaleChain(
false // do not fire events while loading
);
}
Localizer.prototype = {
// * Sets the path to the localization files
//
// * Default Bundle path is “app:/locale/”;
//
// * NOTE: If setLocaleChain hasn’t been called, the directory listing of the bundle path is used to
// automatically figure out what locales are supported by the application and than call
// “sortLanguagesByPreference” in order to sort them using the Preferences defined by the user
// in the “Capabilities.languages” array; If it fails to list the directory it will throw
// “air.Localizer.BundlePathNotFoundError” exception.
//
// * NOTE: In order to have automatically locales detection the path must point to an existing
// directory, that can be listed using runtime’s File API;
setBundlesDirectory: function setBundlesDirectory(/* String */ path){
var file;
try{
file = air.File(path);
}catch(e){
//coercion failed. this must be a string
file = air.File.applicationDirectory.resolvePath(path);
if(!file.exists){ // treating cases like "app:/"
try{
file = new air.File(path);
}catch(e) { ; }
}
}
// Checking that the path points to an existing folder
if(!file||!(file.exists&&file.isDirectory)){
throw new Localizer.BundlePathNotFoundError(path);
}
this._private.clearCache();
this._private.bundlePath = file ;
// autodiscover will not change the locale chain if the developer already called setLocaleChain
this._private.autoDiscoverLocaleChain(
true // it should fire an event when the chain is changed
);
},
// * Walks all the elements in the DOM tree under the specified “parentNode” and applies the localization process:
// * finds all the attributes prefixed with the current prefix (”local_” by default)
// * recreates the attributes by removing the prefix and setting the value to the
// value defined by the specified key in the resource bundle
// * in one particular case “{prefix}innerHTML” the innerHTML will be changed to the value defined by the key;
// * Uses the locale chain returned by “getLocaleChain”.
// * Note: When the “parentNode” is missing the window.document node is used instead.
// * The key should be in the following format: “{bundleName}.{resourceName}”.
//
// E.g. this element:
// <a local_href="default.urlLogin" local_innerHTML="default.cmdLogin"></a>
// becomes
// <a local_href="default.urlLogin" local_innerHTML="default.cmdLogin" href="app:/login.en.html">Click here to login</a>
update: function update(/* DOMElement, optional */ parentNode /* = document */){
this._private.update(parentNode);
},
// * If “locale” argument is provided it is used to return the value of the localization resource
// called “resourceName” located in the bundle called “bundleName” only for the locale “locale”.
// Otherwise, the locale chain returned by “getLocaleChain” is used to lookup the first locale
// that that provides the “resourceName”. For example if the locale chain is [fr_CA, fr] and a
// particular resource is not found in “fr_CA”, the framework will also search that resource in the “fr”.
//
// * It will automatically load the bundles if needed.
//
// * Returns null if the resource is not found and fires one of the following events:
// * RESOURCE_NOT_FOUND when the bundle does not contain the specified “resourceName”;
// * BUNDLE_NOT_FOUND when the bundle file is not found.
//
// * If “templateArgs” is provided the function will use it to replace bracketed numbers in the resource
// with the correspondent values from the “templateArgs” array (only where applicable, meaning
// that if templateArgs[n] is not defined, “{n}” will not be changed):
// * “{0}”, “{1}”, .... “{n}” will be replaced with templateArgs[0], templateArgs[1] ... templateArgs[n].
// * in order to skip replacement for one number, just set that “templateArgs” item to undefined or null;
getString: function getString(/* String */ bundleName, /*String */ resourceName, /* optional String[] */ templateArgs, /* optional, String */ locale ) /*: String*/{
var result = null;
if(locale){
result = this._private.getString(bundleName, resourceName, locale);
}else{
result = this._private.getStringFromChain(bundleName, resourceName);
}
if(templateArgs&&result!=null){
result = this._private.template(result, templateArgs);
}
return result;
},
// * If “locale” argument is provided, it returns the contents of the file
// “{bundlesDirectory}/{locale}/{resourceFileName}”. Otherwise, the locale
// chain returned by “getLocaleChain” is used to lookup the first locale that
// that provides the “resourceFileName” file. For example if the locale chain is
// [fr_FR, fr] the file will be searched using that order: first will search
// “{bundlesDirectory}/fr_FR/{resourceFileName}” and only if it is not found the
// framework will continue to search for “{bundlesDirectory}/fr/{resourceFileName}”.
//
// * {bundlesPath} is the current bundle path set using “setBundlesDirectory”;
//
// * Returns null if the resource file is not found and fires:
// * FILE_NOT_FOUND event;
//
// * If “templateArgs” is provided the function will use it to replace bracketed numbers in the
// resource file with the correspondent values from the “templateArgs” array (only where applicable,
// meaning that if templateArgs[n] is not defined, “{n}” will not be changed):
// * “{0}”, “{1}”, .... “{n}” will be replaced with templateArgs[0], templateArgs[1] ... templateArgs[n].
// * in order to skip replacement for one number, just set that “templateArgs” item to undefined;
getFile: function getFile(/* String */ resourceFileName, /* optional String[] */ templateArgs, /* String */ locale ) /*: String*/ {
var result = null;
if(locale){
result = this._private.getFile(resourceFileName, locale);
}else{
result = this._private.getFileFromChain(resourceFileName);
}
if(templateArgs&&result!=null){
result = this._private.template(result, templateArgs);
}
return result;
},
// * Sets the locale chain and updates the current locale used by all other functions.
//
// * Note: When “chain” argument is missing, is not an array or has zero length the function fails
// and throws “air.Localizer.IllegalArgumentsError” exception
//
// * Fires LOCALE_CHANGE event, the developer should update the DOM using update function. This event
// is fired synchronous whenever the locale chain has changed.
setLocaleChain: function setLocaleChain(/* optional, String[] */ chain){
if(!(chain&&chain.hasOwnProperty&&chain.hasOwnProperty('length')&&chain.length>0)){
throw new Localizer.IllegalArgumentsError("Locale chain should be an array.");
}
this._private.setLocaleChain(chain);
},
// * Returns the locale chain set by the previous call to “setLocaleChain”.
//
// * NOTE: If “setLocaleChain” hasn’t been called the function returns the automatically
// detected locale chain computed when the Localizer is instantiated (see also the property called “localizer”);
getLocaleChain: function getLocaleChain()/* :String[] */ {
return this._private.getLocaleChain();
},
// * Sets the prefix for local attributes used in the “update” function.
//
// * Default prefix is “local_”.
setLocalAttributePrefix: function setLocalAttributePrefix(value){
this._private.attributePrefix = value+'';
},
// typical add/remove Event Dispatcher
addEventListener: function addEventListener(/* String */ eventName, /* callback function */ callback){
this._private.addEventListener(eventName, callback);
},
removeEventListener: function removeEventListener(/* String */ eventName, /* callback function */ callback){
this._private.removeEventListener(eventName, callback);
}
};
// Define the getter function. This will temporararly set canCreateLocalizer to true
// and create a new Localizer. It writes the newly created localizer to the localizerInstance variable;
var localizerInstance = null;
Localizer.__defineGetter__("localizer", function(){
if(!localizerInstance){
canCreateLocalizer = true;
localizerInstance = new Localizer();
canCreateLocalizer = false;
}
return localizerInstance;
});
// EVENTS
// Fired by “setLocaleChain” when the current locale is changed (synchronous)
// Event object strcuture is : { localeChain /* : String [] */ }
Localizer.LOCALE_CHANGE = "change";
// Fired by “getString” and “update” functions when a resource is not found in the specified bundle (fired asynchronous)
// Event object structure is : { resourceName /* : String */, bundleName /* : String */ }
Localizer.RESOURCE_NOT_FOUND = "resourceNotFound";
// Fired by “getFile” when a resource file is not found (fired asynchronous)
// Event object structure is : { resourceFileName /* : String */ }
Localizer.FILE_NOT_FOUND = "fileNotFound";
// Fired by “getString” and “update” functions when a bundle file is not found (fired asynchronous)
// Event object structure is : { bundleName /* : String */ }
Localizer.BUNDLE_NOT_FOUND = "bundleNotFound";
// Version number
Localizer.version = version;
(function(){
// ERROR
function BundlePathNotFoundError(path){
this.name = 'air.Localizer.BundlePathNotFoundError';
this.message = "Bundle directory not found "+path;
}
BundlePathNotFoundError.prototype = new Error;
Localizer.BundlePathNotFoundError = BundlePathNotFoundError;
function IllegalArgumentsError(msg){
this.name = 'air.Localizer.IllegalArgumentsError';
this.message = msg;
}
IllegalArgumentsError.prototype = new Error;
Localizer.IllegalArgumentsError = IllegalArgumentsError;
}());
Localizer.LocaleId = LocaleId;
Localizer.ultimateFallbackLocale = 'en';
// * This function is for internal use only.
//
// * Sort a languages array using the order given by the system capabilities languages array.
//
// * Setting removeUnsupported to true removes system unsupported languages. When removeUnsupported is false it makes the system unsupported languages be the last ones preserving order from original array.
//
// * Default value for removeUnsupported is true.
//
// *NOTE: It uses the same approach that the ADOBE AIR application Installer uses:
// * it tries to find the perfect match for locale name;
// * otherwise it fallbacks to finding locales with same parents (eg. “en_US” will fallback to “en” )
// * user preference will be promoted;
//
// * (system supported means they are in the Capabilities.languages array from the runtime)
//
// * eg: If Capabilities.languages = [ “fr_CA”, “en_UK”, “ja” ] and the “languages” argument
// is [ “en”, “fr_FR”, “zn_ZN” ] the returning array will be [ “fr_FR”, “en” ]
//
// * eg2: When the “languages” argument is [ fr, fr_CA, fr_FR, ro, en, en_US ]:
// * and the Capabilities.languages = [ fr_CA ] the result will be [ fr_CA, fr, fr_FR ]
// * and the Capabilities.languages = [ fr_CA, en ] the result will be [ fr_CA, fr, fr_FR, en, en_US ]
Localizer.sortLanguagesByPreference = function sortLanguagesByPreference( /* String[] */ appLocales, /* String[] */ systemPreferences, /* String, optional */ ultimateFallbackLocale /* = null*/, /*Boolean, optional */ keepAllLocales /* = true */) /* : String[] */{
if(!( appLocales && appLocales.hasOwnProperty('length') && systemPreferences&&systemPreferences.hasOwnProperty('length') )){
throw new Localizer.IllegalArgumentsError("Expected at least two arguments: appLocales and systemPreferences.");
}
return LocaleSorter.sortLocalesUsingPreferences( appLocales, systemPreferences, ultimateFallbackLocale, keepAllLocales)
};
if(!window.air){
window.air = {};
}
window.air.Localizer = Localizer;
}());