ilib
Version:
iLib is a cross-engine library of internationalization (i18n) classes written in pure JS
1,764 lines (1,675 loc) • 1.04 MB
JavaScript
/*< ilib.js */
/*
* ilib.js - define the ilib name space
*
* Copyright © 2012-2015, JEDLSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @namespace The global namespace that contains general ilib functions useful
* to all of ilib
*
* @version "11.0.006"
*/
var ilib = ilib || {};
/** @private */
ilib._ver = function() {
return "11.0.006"
;
};
/**
* Return the current version of ilib.
*
* @static
* @return {string} a version string for this instance of ilib
*/
ilib.getVersion = function () {
// TODO: need some way of getting the version number under dynamic load code
return ilib._ver() || "11.0";
};
/**
* Place where resources and such are eventually assigned.
*/
ilib.data = {
/** @type {{ccc:Object.<string,number>,nfd:Object.<string,string>,nfc:Object.<string,string>,nfkd:Object.<string,string>,nfkc:Object.<string,string>}} */
norm: {
ccc: {},
nfd: {},
nfc: {},
nfkd: {},
nfkc: {}
},
zoneinfo: {
"Etc/UTC":{"o":"0:0","f":"UTC"},
"local":{"f":"local"}
},
/** @type {null|Object.<string,Array.<Array.<number>>>} */ ctype: null,
/** @type {null|Object.<string,Array.<Array.<number>>>} */ ctype_c: null,
/** @type {null|Object.<string,Array.<Array.<number>>>} */ ctype_l: null,
/** @type {null|Object.<string,Array.<Array.<number>>>} */ ctype_m: null,
/** @type {null|Object.<string,Array.<Array.<number>>>} */ ctype_p: null,
/** @type {null|Object.<string,Array.<Array.<number>>>} */ ctype_z: null,
/** @type {null|Object.<string,Array.<Array.<number>>>} */ scriptToRange: null,
/** @type {null|Object.<string,string|Object.<string|Object.<string,string>>>} */ dateformats: null,
/** @type {null|Array.<string>} */ timezones: []
};
/*
if (typeof(window) !== 'undefined') {
window["ilib"] = ilib;
}
*/
// export ilib for use as a module in nodejs
if (typeof(module) !== 'undefined') {
module.exports.ilib = ilib; // for backwards compatibility with older versions of ilib
}
/**
* Sets the pseudo locale. Pseudolocalization (or pseudo-localization) is used for testing
* internationalization aspects of software. Instead of translating the text of the software
* into a foreign language, as in the process of localization, the textual elements of an application
* are replaced with an altered version of the original language.These specific alterations make
* the original words appear readable, but include the most problematic characteristics of
* the world's languages: varying length of text or characters, language direction, and so on.
* Regular Latin pseudo locale: eu-ES and RTL pseudo locale: ps-AF
*
* @param {string|undefined|null} localename the locale specifier for the pseudo locale
*/
ilib.setAsPseudoLocale = function (localename) {
if (localename) {
ilib.pseudoLocales.push(localename)
}
};
/**
* Reset the list of pseudo locales back to the default single locale of zxx-XX.
* @static
*/
ilib.clearPseudoLocales = function() {
ilib.pseudoLocales = [
"zxx-XX",
"zxx-Cyrl-XX",
"zxx-Hans-XX",
"zxx-Hebr-XX"
];
};
ilib.clearPseudoLocales();
/**
* Return the name of the platform
* @private
* @static
* @return {string} string naming the platform
*/
ilib._getPlatform = function () {
if (!ilib._platform) {
try {
if (typeof(java.lang.Object) !== 'undefined') {
ilib._platform = (typeof(process) !== 'undefined') ? "trireme" : "rhino";
return ilib._platform;
}
} catch (e) {}
if (typeof(process) !== 'undefined' && typeof(module) !== 'undefined') {
ilib._platform = "nodejs";
} else if (typeof(Qt) !== 'undefined') {
ilib._platform = "qt";
} else if (typeof(window) !== 'undefined') {
ilib._platform = (typeof(PalmSystem) !== 'undefined') ? "webos" : "browser";
} else {
ilib._platform = "unknown";
}
}
return ilib._platform;
};
/**
* If this ilib is running in a browser, return the name of that browser.
* @private
* @static
* @return {string|undefined} the name of the browser that this is running in ("firefox", "chrome", "ie",
* "safari", or "opera"), or undefined if this is not running in a browser or if
* the browser name could not be determined
*/
ilib._getBrowser = function () {
var browser = undefined;
if (ilib._getPlatform() === "browser") {
if (navigator && navigator.userAgent) {
if (navigator.userAgent.indexOf("Firefox") > -1) {
browser = "firefox";
}
if (navigator.userAgent.indexOf("Opera") > -1) {
browser = "opera";
}
if (navigator.userAgent.indexOf("Chrome") > -1) {
browser = "chrome";
}
if (navigator.userAgent.indexOf(" .NET") > -1) {
browser = "ie";
}
if (navigator.userAgent.indexOf("Safari") > -1) {
// chrome also has the string Safari in its userAgent, but the chrome case is
// already taken care of above
browser = "safari";
}
}
}
return browser;
};
/**
* Return true if the global variable is defined on this platform.
* @private
* @static
* @param {string} name the name of the variable to check
* @return {boolean} true if the global variable is defined on this platform, false otherwise
*/
ilib._isGlobal = function(name) {
switch (ilib._getPlatform()) {
case "rhino":
var top = (function() {
return (typeof global === 'object') ? global : this;
})();
return typeof(top[name]) !== 'undefined';
case "nodejs":
case "trireme":
var root = typeof(global) !== 'undefined' ? global : this;
return root && typeof(root[name]) !== 'undefined';
case "qt":
return false;
default:
try {
return window && typeof(window[name]) !== 'undefined';
} catch (e) {
return false;
}
}
};
/**
* Sets the default locale for all of ilib. This locale will be used
* when no explicit locale is passed to any ilib class. If the default
* locale is not set, ilib will attempt to use the locale of the
* environment it is running in, if it can find that. If not, it will
* default to the locale "en-US". If a type of parameter is string,
* ilib will take only well-formed BCP-47 tag <p>
*
*
* @static
* @param {string|undefined|null} spec the locale specifier for the default locale
*/
ilib.setLocale = function (spec) {
if (typeof(spec) === 'string' || !spec) {
ilib.locale = spec;
}
// else ignore other data types, as we don't have the dependencies
// to look into them to find a locale
};
/**
* Return the default locale for all of ilib if one has been set. This
* locale will be used when no explicit locale is passed to any ilib
* class. If the default
* locale is not set, ilib will attempt to use the locale of the
* environment it is running in, if it can find that. If not, it will
* default to the locale "en-US".<p>
*
*
* @static
* @return {string} the locale specifier for the default locale
*/
ilib.getLocale = function () {
if (typeof(ilib.locale) !== 'string') {
var plat = ilib._getPlatform();
switch (plat) {
case 'browser':
// running in a browser
ilib.locale = navigator.language.substring(0,3) + navigator.language.substring(3,5).toUpperCase(); // FF/Opera/Chrome/Webkit
if (!ilib.locale) {
// IE on Windows
var lang = typeof(navigator.browserLanguage) !== 'undefined' ?
navigator.browserLanguage :
(typeof(navigator.userLanguage) !== 'undefined' ?
navigator.userLanguage :
(typeof(navigator.systemLanguage) !== 'undefined' ?
navigator.systemLanguage :
undefined));
if (typeof(lang) !== 'undefined' && lang) {
// for some reason, MS uses lower case region tags
ilib.locale = lang.substring(0,3) + lang.substring(3,5).toUpperCase();
}
}
break;
case 'webos':
// webOS
if (typeof(PalmSystem.locales) !== 'undefined' &&
typeof(PalmSystem.locales.UI) != 'undefined' &&
PalmSystem.locales.UI.length > 0) {
ilib.locale = PalmSystem.locales.UI;
} else if (typeof(PalmSystem.locale) !== 'undefined') {
ilib.locale = PalmSystem.locale;
}
break;
case 'rhino':
if (typeof(environment) !== 'undefined' && environment.user && typeof(environment.user.language) === 'string' && environment.user.language.length > 0) {
// running under plain rhino
ilib.locale = environment.user.language;
if (typeof(environment.user.country) === 'string' && environment.user.country.length > 0) {
ilib.locale += '-' + environment.user.country;
}
}
break;
case "trireme":
// under trireme on rhino emulating nodejs
var lang = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL;
// the LANG variable on unix is in the form "lang_REGION.CHARSET"
// where language and region are the correct ISO codes separated by
// an underscore. This translate it back to the BCP-47 form.
if (lang && typeof(lang) !== 'undefined') {
ilib.locale = lang.substring(0,2).toLowerCase() + '-' + lang.substring(3,5).toUpperCase();
}
break;
case 'nodejs':
// running under nodejs
var lang = process.env.LANG || process.env.LC_ALL;
// the LANG variable on unix is in the form "lang_REGION.CHARSET"
// where language and region are the correct ISO codes separated by
// an underscore. This translate it back to the BCP-47 form.
if (lang && typeof(lang) !== 'undefined') {
ilib.locale = lang.substring(0,2).toLowerCase() + '-' + lang.substring(3,5).toUpperCase();
}
break;
case 'qt':
// running in the Javascript engine under Qt/QML
var locobj = Qt.locale();
var lang = locobj.name && locobj.name.replace("_", "-") || "en-US";
break;
}
ilib.locale = typeof(ilib.locale) === 'string' ? ilib.locale : 'en-US';
}
return ilib.locale;
};
/**
* Sets the default time zone for all of ilib. This time zone will be used when
* no explicit time zone is passed to any ilib class. If the default time zone
* is not set, ilib will attempt to use the time zone of the
* environment it is running in, if it can find that. If not, it will
* default to the the UTC zone "Etc/UTC".<p>
*
*
* @static
* @param {string} tz the name of the time zone to set as the default time zone
*/
ilib.setTimeZone = function (tz) {
ilib.tz = tz || ilib.tz;
};
/**
* Return the default time zone for all of ilib if one has been set. This
* time zone will be used when no explicit time zone is passed to any ilib
* class. If the default time zone
* is not set, ilib will attempt to use the locale of the
* environment it is running in, if it can find that. If not, it will
* default to the the zone "local".<p>
*
*
* @static
* @return {string} the default time zone for ilib
*/
ilib.getTimeZone = function() {
if (typeof(ilib.tz) === 'undefined') {
if (typeof(navigator) !== 'undefined' && typeof(navigator.timezone) !== 'undefined') {
// running in a browser
if (navigator.timezone.length > 0) {
ilib.tz = navigator.timezone;
}
} else if (typeof(PalmSystem) !== 'undefined' && typeof(PalmSystem.timezone) !== 'undefined') {
// running in webkit on webOS
if (PalmSystem.timezone.length > 0) {
ilib.tz = PalmSystem.timezone;
}
} else if (typeof(environment) !== 'undefined' && typeof(environment.user) !== 'undefined') {
// running under rhino
if (typeof(environment.user.timezone) !== 'undefined' && environment.user.timezone.length > 0) {
ilib.tz = environment.user.timezone;
}
} else if (typeof(process) !== 'undefined' && typeof(process.env) !== 'undefined') {
// running in nodejs
if (process.env.TZ && typeof(process.env.TZ) !== "undefined") {
ilib.tz = process.env.TZ;
}
}
ilib.tz = ilib.tz || "local";
}
return ilib.tz;
};
/**
* @class
* Defines the interface for the loader class for ilib. The main method of the
* loader object is loadFiles(), which loads a set of requested locale data files
* from where-ever it is stored.
* @interface
*/
ilib.Loader = function() {};
/**
* Load a set of files from where-ever it is stored.<p>
*
* This is the main function define a callback function for loading missing locale
* data or resources.
* If this copy of ilib is assembled without including the required locale data
* or resources, then that data can be lazy loaded dynamically when it is
* needed by calling this method. Each ilib class will first
* check for the existence of data under ilib.data, and if it is not there,
* it will attempt to load it by calling this method of the laoder, and then place
* it there.<p>
*
* Suggested implementations of this method might load files
* directly from disk under nodejs or rhino, or within web pages, to load
* files from the server with XHR calls.<p>
*
* The first parameter to this method, paths, is an array of relative paths within
* the ilib dir structure for the
* requested data. These paths will already have the locale spec integrated
* into them, so no further tweaking needs to happen to load the data. Simply
* load the named files. The second
* parameter tells the loader whether to load the files synchronously or asynchronously.
* If the sync parameters is false, then the onLoad function must also be specified.
* The third parameter gives extra parameters to the loader passed from the calling
* code. This may contain any property/value pairs. The last parameter, callback,
* is a callback function to call when all of the data is finishing loading. Make
* sure to call the callback with the context of "this" so that the caller has their
* context back again.<p>
*
* The loader function must be able to operate either synchronously or asychronously.
* If the loader function is called with an undefined callback function, it is
* expected to load the data synchronously, convert it to javascript
* objects, and return the array of json objects as the return value of the
* function. If the loader
* function is called with a callback function, it may load the data
* synchronously or asynchronously (doesn't matter which) as long as it calls
* the callback function with the data converted to a javascript objects
* when it becomes available. If a particular file could not be loaded, the
* loader function should put undefined into the corresponding entry in the
* results array.
* Note that it is important that all the data is loaded before the callback
* is called.<p>
*
* An example implementation for nodejs might be:
*
* <pre>
* *
* var myLoader = function() {};
* myLoader.prototype = new Loader();
* myLoader.prototype.constructor = myLoader;
* myLoader.prototype.loadFiles = function(paths, sync, params, callback) {
* if (sync) {
* var ret = [];
* // synchronous load -- just return the result
* paths.forEach(function (path) {
* var json = fs.readFileSync(path, "utf-8");
* ret.push(json ? JSON.parse(json) : undefined);
* });
*
* return ret;
* }
* this.callback = callback;
*
* // asynchronous
* this.results = [];
* this._loadFilesAsync(paths);
* }
* myLoader.prototype._loadFilesAsync = function (paths) {
* if (paths.length > 0) {
* var file = paths.shift();
* fs.readFile(file, "utf-8", function(err, json) {
* this.results.push(err ? undefined : JSON.parse(json));
* // call self recursively so that the callback is only called at the end
* // when all the files are loaded sequentially
* if (paths.length > 0) {
* this._loadFilesAsync(paths);
* } else {
* this.callback(this.results);
* }
* });
* }
* }
*
* // bind to "this" so that "this" is relative to your own instance
* ilib.setLoaderCallback(new myLoader());
* </pre>
* @param {Array.<string>} paths An array of paths to load from wherever the files are stored
* @param {Boolean} sync if true, load the files synchronously, and false means asynchronously
* @param {Object} params an object with any extra parameters for the loader. These can be
* anything. The caller of the ilib class passes these parameters in. Presumably, the code that
* calls ilib and the code that provides the loader are together and can have a private
* agreement between them about what the parameters should contain.
* @param {function(Object)} callback function to call when the files are all loaded. The
* parameter of the callback function is the contents of the files.
*/
ilib.Loader.prototype.loadFiles = function (paths, sync, params, callback) {};
/**
* Return all files available for loading using this loader instance.
* This method returns an object where the properties are the paths to
* directories where files are loaded from and the values are an array
* of strings containing the relative paths under the directory of each
* file that can be loaded.<p>
*
* Example:
* <pre>
* {
* "/usr/share/javascript/ilib/locale": [
* "dateformats.json",
* "aa/dateformats.json",
* "af/dateformats.json",
* "agq/dateformats.json",
* "ak/dateformats.json",
* ...
* "zxx/dateformats.json"
* ]
* }
* </pre>
* @returns {Object} a hash containing directory names and
* paths to file that can be loaded by this loader
*/
ilib.Loader.prototype.listAvailableFiles = function() {};
/**
* Return true if the file in the named path is available for loading using
* this loader. The path may be given as an absolute path, in which case
* only that file is checked, or as a relative path, in which case, the
* relative path may appear underneath any of the directories that the loader
* knows about.
* @returns {boolean} true if the file in the named path is available for loading, and
* false otherwise
*/
ilib.Loader.prototype.isAvailable = function(path) {};
/**
* Set the custom loader used to load ilib's locale data in your environment.
* The instance passed in must implement the Loader interface. See the
* Loader class documentation for more information about loaders.
*
* @static
* @param {ilib.Loader} loader class to call to access the requested data.
* @return {boolean} true if the loader was installed correctly, or false
* if not
*/
ilib.setLoaderCallback = function(loader) {
// only a basic check
if ((typeof(loader) === 'object' && typeof(loader.loadFiles) === 'function') ||
typeof(loader) === 'function' || typeof(loader) === 'undefined') {
//console.log("setting callback loader to " + (loader ? loader.name : "undefined"));
ilib._load = loader;
return true;
}
return false;
};
/**
* Return the custom Loader instance currently in use with this instance
* of ilib. If there is no loader, this method returns undefined.
*
* @protected
* @static
* @return {ilib.Loader|undefined} the loader instance currently in use, or
* undefined if there is no such loader
*/
ilib.getLoader = function() {
return ilib._load;
};
/**
* Test whether an object in an javascript array.
*
* @static
* @param {*} object The object to test
* @return {boolean} return true if the object is an array
* and false otherwise
*/
ilib.isArray = function(object) {
var o;
if (typeof(object) === 'object') {
o = /** @type {Object|null|undefined} */ object;
return Object.prototype.toString.call(o) === '[object Array]';
}
return false;
};
/**
* Extend object1 by mixing in everything from object2 into it. The objects
* are deeply extended, meaning that this method recursively descends the
* tree in the objects and mixes them in at each level. Arrays are extended
* by concatenating the elements of object2 onto those of object1.
*
* @static
* @param {Object} object1 the target object to extend
* @param {Object=} object2 the object to mix in to object1
* @return {Object} returns object1
*/
ilib.extend = function (object1, object2) {
var prop = undefined;
if (object2) {
for (prop in object2) {
// don't extend object with undefined or functions
if (prop && typeof(object2[prop]) !== 'undefined' && typeof(object2[prop]) !== "function") {
if (ilib.isArray(object1[prop]) && ilib.isArray(object2[prop])) {
//console.log("Merging array prop " + prop);
object1[prop] = object1[prop].concat(object2[prop]);
} else if (typeof(object1[prop]) === 'object' && typeof(object2[prop]) === 'object') {
//console.log("Merging object prop " + prop);
if (prop !== "ilib") {
object1[prop] = ilib.extend(object1[prop], object2[prop]);
}
} else {
//console.log("Copying prop " + prop);
// for debugging. Used to determine whether or not json files are overriding their parents unnecessarily
object1[prop] = object2[prop];
}
}
}
}
return object1;
};
ilib.extend2 = function (object1, object2) {
var prop = undefined;
if (object2) {
for (prop in object2) {
// don't extend object with undefined or functions
if (prop && typeof(object2[prop]) !== 'undefined') {
if (ilib.isArray(object1[prop]) && ilib.isArray(object2[prop])) {
//console.log("Merging array prop " + prop);
object1[prop] = object1[prop].concat(object2[prop]);
} else if (typeof(object1[prop]) === 'object' && typeof(object2[prop]) === 'object') {
//console.log("Merging object prop " + prop);
if (prop !== "ilib") {
object1[prop] = ilib.extend2(object1[prop], object2[prop]);
}
} else {
//console.log("Copying prop " + prop);
// for debugging. Used to determine whether or not json files are overriding their parents unnecessarily
object1[prop] = object2[prop];
}
}
}
}
return object1;
};
/**
* If Function.prototype.bind does not exist in this JS engine, this
* function reimplements it in terms of older JS functions.
* bind() doesn't exist in many older browsers.
*
* @static
* @param {Object} scope object that the method should operate on
* @param {function(...)} method method to call
* @return {function(...)|undefined} function that calls the given method
* in the given scope with all of its arguments properly attached, or
* undefined if there was a problem with the arguments
*/
ilib.bind = function(scope, method/*, bound arguments*/){
if (!scope || !method) {
return undefined;
}
/** @protected
* @param {Arguments} inArrayLike
* @param {number=} inOffset
*/
function cloneArray(inArrayLike, inOffset) {
var arr = [];
for(var i = inOffset || 0, l = inArrayLike.length; i<l; i++){
arr.push(inArrayLike[i]);
}
return arr;
}
if (typeof(method) === 'function') {
var func, args = cloneArray(arguments, 2);
if (typeof(method.bind) === 'function') {
func = method.bind.apply(method, [scope].concat(args));
} else {
func = function() {
var nargs = cloneArray(arguments);
// invoke with collected args
return method.apply(scope, args.concat(nargs));
};
}
return func;
}
return undefined;
};
/**
* @private
*/
ilib._dyncode = false;
/**
* Return true if this copy of ilib is using dynamically loaded code. It returns
* false for pre-assembled code.
*
* @static
* @return {boolean} true if this ilib uses dynamically loaded code, and false otherwise
*/
ilib.isDynCode = function() {
return ilib._dyncode;
};
/**
* @private
*/
ilib._dyndata = false;
/**
* Return true if this copy of ilib is using dynamically loaded locale data. It returns
* false for pre-assembled data.
*
* @static
* @return {boolean} true if this ilib uses dynamically loaded locale data, and false otherwise
*/
ilib.isDynData = function() {
return ilib._dyndata;
};
ilib._loadtime = new Date().getTime();
/*< JSUtils.js */
/*
* JSUtils.js - Misc utilities to work around Javascript engine differences
*
* Copyright © 2013-2015, JEDLSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// !depends ilib.js
var JSUtils = {};
/**
* Perform a shallow copy of the source object to the target object. This only
* copies the assignments of the source properties to the target properties,
* but not recursively from there.<p>
*
*
* @static
* @param {Object} source the source object to copy properties from
* @param {Object} target the target object to copy properties into
*/
JSUtils.shallowCopy = function (source, target) {
var prop = undefined;
if (source && target) {
for (prop in source) {
if (prop !== undefined && typeof(source[prop]) !== 'undefined') {
target[prop] = source[prop];
}
}
}
};
/**
* Perform a recursive deep copy from the "from" object to the "deep" object.
*
* @static
* @param {Object} from the object to copy from
* @param {Object} to the object to copy to
* @return {Object} a reference to the the "to" object
*/
JSUtils.deepCopy = function(from, to) {
var prop;
for (prop in from) {
if (prop) {
if (typeof(from[prop]) === 'object') {
to[prop] = {};
JSUtils.deepCopy(from[prop], to[prop]);
} else {
to[prop] = from[prop];
}
}
}
return to;
};
/**
* Map a string to the given set of alternate characters. If the target set
* does not contain a particular character in the input string, then that
* character will be copied to the output unmapped.
*
* @static
* @param {string} str a string to map to an alternate set of characters
* @param {Array.<string>|Object} map a mapping to alternate characters
* @return {string} the source string where each character is mapped to alternate characters
*/
JSUtils.mapString = function (str, map) {
var mapped = "";
if (map && str) {
for (var i = 0; i < str.length; i++) {
var c = str.charAt(i); // TODO use a char iterator?
mapped += map[c] || c;
}
} else {
mapped = str;
}
return mapped;
};
/**
* Check if an object is a member of the given array. If this javascript engine
* support indexOf, it is used directly. Otherwise, this function implements it
* itself. The idea is to make sure that you can use the quick indexOf if it is
* available, but use a slower implementation in older engines as well.
*
* @static
* @param {Array.<Object>} array array to search
* @param {Object} obj object being sought. This should be of the same type as the
* members of the array being searched. If not, this function will not return
* any results.
* @return {number} index of the object in the array, or -1 if it is not in the array.
*/
JSUtils.indexOf = function(array, obj) {
if (!array || !obj) {
return -1;
}
if (typeof(array.indexOf) === 'function') {
return array.indexOf(obj);
} else {
for (var i = 0; i < array.length; i++) {
if (array[i] === obj) {
return i;
}
}
return -1;
}
};
/**
* Convert a string into the hexadecimal representation
* of the Unicode characters in that string.
*
* @static
* @param {string} string The string to convert
* @param {number=} limit the number of digits to use to represent the character (1 to 8)
* @return {string} a hexadecimal representation of the
* Unicode characters in the input string
*/
JSUtils.toHexString = function(string, limit) {
var i,
result = "",
lim = (limit && limit < 9) ? limit : 4;
if (!string) {
return "";
}
for (i = 0; i < string.length; i++) {
var ch = string.charCodeAt(i).toString(16);
result += "00000000".substring(0, lim-ch.length) + ch;
}
return result.toUpperCase();
};
/**
* Test whether an object in a Javascript Date.
*
* @static
* @param {*} object The object to test
* @return {boolean} return true if the object is a Date
* and false otherwise
*/
JSUtils.isDate = function(object) {
var o;
if (typeof(object) === 'object') {
o = /** @type {Object|null|undefined} */ object;
return Object.prototype.toString.call(o) === '[object Date]';
}
return false;
};
/**
* Merge the properties of object2 into object1 in a deep manner and return a merged
* object. If the property exists in both objects, the value in object2 will overwrite
* the value in object1. If a property exists in object1, but not in object2, its value
* will not be touched. If a property exists in object2, but not in object1, it will be
* added to the merged result.<p>
*
* Name1 and name2 are for creating debug output only. They are not necessary.<p>
*
*
* @static
* @param {*} object1 the object to merge into
* @param {*} object2 the object to merge
* @param {boolean=} replace if true, replace the array elements in object1 with those in object2.
* If false, concatenate array elements in object1 with items in object2.
* @param {string=} name1 name of the object being merged into
* @param {string=} name2 name of the object being merged in
* @return {Object} the merged object
*/
JSUtils.merge = function (object1, object2, replace, name1, name2) {
var prop = undefined,
newObj = {};
for (prop in object1) {
if (prop && typeof(object1[prop]) !== 'undefined') {
newObj[prop] = object1[prop];
}
}
for (prop in object2) {
if (prop && typeof(object2[prop]) !== 'undefined') {
if (ilib.isArray(object1[prop]) && ilib.isArray(object2[prop])) {
if (typeof(replace) !== 'boolean' || !replace) {
newObj[prop] = [].concat(object1[prop]);
newObj[prop] = newObj[prop].concat(object2[prop]);
} else {
newObj[prop] = object2[prop];
}
} else if (typeof(object1[prop]) === 'object' && typeof(object2[prop]) === 'object') {
newObj[prop] = JSUtils.merge(object1[prop], object2[prop], replace);
} else {
// for debugging. Used to determine whether or not json files are overriding their parents unnecessarily
if (name1 && name2 && newObj[prop] == object2[prop]) {
console.log("Property " + prop + " in " + name1 + " is being overridden by the same value in " + name2);
}
newObj[prop] = object2[prop];
}
}
}
return newObj;
};
/**
* Return true if the given object has no properties.<p>
*
*
* @static
* @param {Object} obj the object to check
* @return {boolean} true if the given object has no properties, false otherwise
*/
JSUtils.isEmpty = function (obj) {
var prop = undefined;
if (!obj) {
return true;
}
for (prop in obj) {
if (prop && typeof(obj[prop]) !== 'undefined') {
return false;
}
}
return true;
};
/**
* @static
*/
JSUtils.hashCode = function(obj) {
var hash = 0;
function addHash(hash, newValue) {
// co-prime numbers creates a nicely distributed hash
hash *= 65543;
hash += newValue;
hash %= 2147483647;
return hash;
}
function stringHash(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = addHash(hash, str.charCodeAt(i));
}
return hash;
}
switch (typeof(obj)) {
case 'undefined':
hash = 0;
break;
case 'string':
hash = stringHash(obj);
break;
case 'function':
case 'number':
case 'xml':
hash = stringHash(String(obj));
break;
case 'boolean':
hash = obj ? 1 : 0;
break;
case 'object':
var props = [];
for (var p in obj) {
if (obj.hasOwnProperty(p)) {
props.push(p);
}
}
// make sure the order of the properties doesn't matter
props.sort();
for (var i = 0; i < props.length; i++) {
hash = addHash(hash, stringHash(props[i]));
hash = addHash(hash, JSUtils.hashCode(obj[props[i]]));
}
break;
}
return hash;
};
/*< Locale.js */
/*
* Locale.js - Locale specifier definition
*
* Copyright © 2012-2015, JEDLSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// !depends ilib.js JSUtils.js
/**
* @class
* Create a new locale instance. Locales are specified either with a specifier string
* that follows the BCP-47 convention (roughly: "language-region-script-variant") or
* with 4 parameters that specify the language, region, variant, and script individually.<p>
*
* The language is given as an ISO 639-1 two-letter, lower-case language code. You
* can find a full list of these codes at
* <a href="http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes</a><p>
*
* The region is given as an ISO 3166-1 two-letter, upper-case region code. You can
* find a full list of these codes at
* <a href="http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2</a>.<p>
*
* The variant is any string that does not contain a dash which further differentiates
* locales from each other.<p>
*
* The script is given as the ISO 15924 four-letter script code. In some locales,
* text may be validly written in more than one script. For example, Serbian is often
* written in both Latin and Cyrillic, though not usually mixed together. You can find a
* full list of these codes at
* <a href="http://en.wikipedia.org/wiki/ISO_15924#List_of_codes">http://en.wikipedia.org/wiki/ISO_15924#List_of_codes</a>.<p>
*
* As an example in ilib, the script can be used in the date formatter. Dates formatted
* in Serbian could have day-of-week names or month names written in the Latin
* or Cyrillic script. Often one script is default such that sr-SR-Latn is the same
* as sr-SR so the script code "Latn" can be left off of the locale spec.<p>
*
* Each part is optional, and an empty string in the specifier before or after a
* dash or as a parameter to the constructor denotes an unspecified value. In this
* case, many of the ilib functions will treat the locale as generic. For example
* the locale "en-" is equivalent to "en" and to "en--" and denotes a locale
* of "English" with an unspecified region and variant, which typically matches
* any region or variant.<p>
*
* Without any arguments to the constructor, this function returns the locale of
* the host Javascript engine.<p>
*
*
* @constructor
* @param {?string|Locale=} language the ISO 639 2-letter code for the language, or a full
* locale spec in BCP-47 format, or another Locale instance to copy from
* @param {string=} region the ISO 3166 2-letter code for the region
* @param {string=} variant the name of the variant of this locale, if any
* @param {string=} script the ISO 15924 code of the script for this locale, if any
*/
var Locale = function(language, region, variant, script) {
if (typeof(region) === 'undefined') {
var spec = language || ilib.getLocale();
if (typeof(spec) === 'string') {
var parts = spec.split('-');
for ( var i = 0; i < parts.length; i++ ) {
if (Locale._isLanguageCode(parts[i])) {
/**
* @private
* @type {string|undefined}
*/
this.language = parts[i];
} else if (Locale._isRegionCode(parts[i])) {
/**
* @private
* @type {string|undefined}
*/
this.region = parts[i];
} else if (Locale._isScriptCode(parts[i])) {
/**
* @private
* @type {string|undefined}
*/
this.script = parts[i];
} else {
/**
* @private
* @type {string|undefined}
*/
this.variant = parts[i];
}
}
this.language = this.language || undefined;
this.region = this.region || undefined;
this.script = this.script || undefined;
this.variant = this.variant || undefined;
} else if (typeof(spec) === 'object') {
this.language = spec.language || undefined;
this.region = spec.region || undefined;
this.script = spec.script || undefined;
this.variant = spec.variant || undefined;
}
} else {
if (language) {
language = language.trim();
this.language = language.length > 0 ? language.toLowerCase() : undefined;
} else {
this.language = undefined;
}
if (region) {
region = region.trim();
this.region = region.length > 0 ? region.toUpperCase() : undefined;
} else {
this.region = undefined;
}
if (variant) {
variant = variant.trim();
this.variant = variant.length > 0 ? variant : undefined;
} else {
this.variant = undefined;
}
if (script) {
script = script.trim();
this.script = script.length > 0 ? script : undefined;
} else {
this.script = undefined;
}
}
this._genSpec();
};
// from http://en.wikipedia.org/wiki/ISO_3166-1
Locale.a2toa3regmap = {
"AF": "AFG",
"AX": "ALA",
"AL": "ALB",
"DZ": "DZA",
"AS": "ASM",
"AD": "AND",
"AO": "AGO",
"AI": "AIA",
"AQ": "ATA",
"AG": "ATG",
"AR": "ARG",
"AM": "ARM",
"AW": "ABW",
"AU": "AUS",
"AT": "AUT",
"AZ": "AZE",
"BS": "BHS",
"BH": "BHR",
"BD": "BGD",
"BB": "BRB",
"BY": "BLR",
"BE": "BEL",
"BZ": "BLZ",
"BJ": "BEN",
"BM": "BMU",
"BT": "BTN",
"BO": "BOL",
"BQ": "BES",
"BA": "BIH",
"BW": "BWA",
"BV": "BVT",
"BR": "BRA",
"IO": "IOT",
"BN": "BRN",
"BG": "BGR",
"BF": "BFA",
"BI": "BDI",
"KH": "KHM",
"CM": "CMR",
"CA": "CAN",
"CV": "CPV",
"KY": "CYM",
"CF": "CAF",
"TD": "TCD",
"CL": "CHL",
"CN": "CHN",
"CX": "CXR",
"CC": "CCK",
"CO": "COL",
"KM": "COM",
"CG": "COG",
"CD": "COD",
"CK": "COK",
"CR": "CRI",
"CI": "CIV",
"HR": "HRV",
"CU": "CUB",
"CW": "CUW",
"CY": "CYP",
"CZ": "CZE",
"DK": "DNK",
"DJ": "DJI",
"DM": "DMA",
"DO": "DOM",
"EC": "ECU",
"EG": "EGY",
"SV": "SLV",
"GQ": "GNQ",
"ER": "ERI",
"EE": "EST",
"ET": "ETH",
"FK": "FLK",
"FO": "FRO",
"FJ": "FJI",
"FI": "FIN",
"FR": "FRA",
"GF": "GUF",
"PF": "PYF",
"TF": "ATF",
"GA": "GAB",
"GM": "GMB",
"GE": "GEO",
"DE": "DEU",
"GH": "GHA",
"GI": "GIB",
"GR": "GRC",
"GL": "GRL",
"GD": "GRD",
"GP": "GLP",
"GU": "GUM",
"GT": "GTM",
"GG": "GGY",
"GN": "GIN",
"GW": "GNB",
"GY": "GUY",
"HT": "HTI",
"HM": "HMD",
"VA": "VAT",
"HN": "HND",
"HK": "HKG",
"HU": "HUN",
"IS": "ISL",
"IN": "IND",
"ID": "IDN",
"IR": "IRN",
"IQ": "IRQ",
"IE": "IRL",
"IM": "IMN",
"IL": "ISR",
"IT": "ITA",
"JM": "JAM",
"JP": "JPN",
"JE": "JEY",
"JO": "JOR",
"KZ": "KAZ",
"KE": "KEN",
"KI": "KIR",
"KP": "PRK",
"KR": "KOR",
"KW": "KWT",
"KG": "KGZ",
"LA": "LAO",
"LV": "LVA",
"LB": "LBN",
"LS": "LSO",
"LR": "LBR",
"LY": "LBY",
"LI": "LIE",
"LT": "LTU",
"LU": "LUX",
"MO": "MAC",
"MK": "MKD",
"MG": "MDG",
"MW": "MWI",
"MY": "MYS",
"MV": "MDV",
"ML": "MLI",
"MT": "MLT",
"MH": "MHL",
"MQ": "MTQ",
"MR": "MRT",
"MU": "MUS",
"YT": "MYT",
"MX": "MEX",
"FM": "FSM",
"MD": "MDA",
"MC": "MCO",
"MN": "MNG",
"ME": "MNE",
"MS": "MSR",
"MA": "MAR",
"MZ": "MOZ",
"MM": "MMR",
"NA": "NAM",
"NR": "NRU",
"NP": "NPL",
"NL": "NLD",
"NC": "NCL",
"NZ": "NZL",
"NI": "NIC",
"NE": "NER",
"NG": "NGA",
"NU": "NIU",
"NF": "NFK",
"MP": "MNP",
"NO": "NOR",
"OM": "OMN",
"PK": "PAK",
"PW": "PLW",
"PS": "PSE",
"PA": "PAN",
"PG": "PNG",
"PY": "PRY",
"PE": "PER",
"PH": "PHL",
"PN": "PCN",
"PL": "POL",
"PT": "PRT",
"PR": "PRI",
"QA": "QAT",
"RE": "REU",
"RO": "ROU",
"RU": "RUS",
"RW": "RWA",
"BL": "BLM",
"SH": "SHN",
"KN": "KNA",
"LC": "LCA",
"MF": "MAF",
"PM": "SPM",
"VC": "VCT",
"WS": "WSM",
"SM": "SMR",
"ST": "STP",
"SA": "SAU",
"SN": "SEN",
"RS": "SRB",
"SC": "SYC",
"SL": "SLE",
"SG": "SGP",
"SX": "SXM",
"SK": "SVK",
"SI": "SVN",
"SB": "SLB",
"SO": "SOM",
"ZA": "ZAF",
"GS": "SGS",
"SS": "SSD",
"ES": "ESP",
"LK": "LKA",
"SD": "SDN",
"SR": "SUR",
"SJ": "SJM",
"SZ": "SWZ",
"SE": "SWE",
"CH": "CHE",
"SY": "SYR",
"TW": "TWN",
"TJ": "TJK",
"TZ": "TZA",
"TH": "THA",
"TL": "TLS",
"TG": "TGO",
"TK": "TKL",
"TO": "TON",
"TT": "TTO",
"TN": "TUN",
"TR": "TUR",
"TM": "TKM",
"TC": "TCA",
"TV": "TUV",
"UG": "UGA",
"UA": "UKR",
"AE": "ARE",
"GB": "GBR",
"US": "USA",
"UM": "UMI",
"UY": "URY",
"UZ": "UZB",
"VU": "VUT",
"VE": "VEN",
"VN": "VNM",
"VG": "VGB",
"VI": "VIR",
"WF": "WLF",
"EH": "ESH",
"YE": "YEM",
"ZM": "ZMB",
"ZW": "ZWE"
};
Locale.a1toa3langmap = {
"ab": "abk",
"aa": "aar",
"af": "afr",
"ak": "aka",
"sq": "sqi",
"am": "amh",
"ar": "ara",
"an": "arg",
"hy": "hye",
"as": "asm",
"av": "ava",
"ae": "ave",
"ay": "aym",
"az": "aze",
"bm": "bam",
"ba": "bak",
"eu": "eus",
"be": "bel",
"bn": "ben",
"bh": "bih",
"bi": "bis",
"bs": "bos",
"br": "bre",
"bg": "bul",
"my": "mya",
"ca": "cat",
"ch": "cha",
"ce": "che",
"ny": "nya",
"zh": "zho",
"cv": "chv",
"kw": "cor",
"co": "cos",
"cr": "cre",
"hr": "hrv",
"cs": "ces",
"da": "dan",
"dv": "div",
"nl": "nld",
"dz": "dzo",
"en": "eng",
"eo": "epo",
"et": "est",
"ee": "ewe",
"fo": "fao",
"fj": "fij",
"fi": "fin",
"fr": "fra",
"ff": "ful",
"gl": "glg",
"ka": "kat",
"de": "deu",
"el": "ell",
"gn": "grn",
"gu": "guj",
"ht": "hat",
"ha": "hau",
"he": "heb",
"hz": "her",
"hi": "hin",
"ho": "hmo",
"hu": "hun",
"ia": "ina",
"id": "ind",
"ie": "ile",
"ga": "gle",
"ig": "ibo",
"ik": "ipk",
"io": "ido",
"is": "isl",
"it": "ita",
"iu": "iku",
"ja": "jpn",
"jv": "jav",
"kl": "kal",
"kn": "kan",
"kr": "kau",
"ks": "kas",
"kk": "kaz",
"km": "khm",
"ki": "kik",
"rw": "kin",
"ky": "kir",
"kv": "kom",
"kg": "kon",
"ko": "kor",
"ku": "kur",
"kj": "kua",
"la": "lat",
"lb": "ltz",
"lg": "lug",
"li": "lim",
"ln": "lin",
"lo": "lao",
"lt": "lit",
"lu": "lub",
"lv": "lav",
"gv": "glv",
"mk": "mkd",
"mg": "mlg",
"ms": "msa",
"ml": "mal",
"mt": "mlt",
"mi": "mri",
"mr": "mar",
"mh": "mah",
"mn": "mon",
"na": "nau",
"nv": "nav",
"nb": "nob",
"nd": "nde",
"ne": "nep",
"ng": "ndo",
"nn": "nno",
"no": "nor",
"ii": "iii",
"nr": "nbl",
"oc": "oci",
"oj": "oji",
"cu": "chu",
"om": "orm",
"or": "ori",
"os": "oss",
"pa": "pan",
"pi": "pli",
"fa": "fas",
"pl": "pol",
"ps": "pus",
"pt": "por",
"qu": "que",
"rm": "roh",
"rn": "run",
"ro": "ron",
"ru": "rus",
"sa": "san",
"sc": "srd",
"sd": "snd",
"se": "sme",
"sm": "smo",
"sg": "sag",
"sr": "srp",
"gd": "gla",
"sn": "sna",
"si": "sin",
"sk": "slk",
"sl": "slv",
"so": "som",
"st": "sot",
"az": "azb",
"es": "spa",
"su": "sun",
"sw": "swa",
"ss": "ssw",
"sv": "swe",
"ta": "tam",
"te": "tel",
"tg": "tgk",
"th": "tha",
"ti": "tir",
"bo": "bod",
"tk": "tuk",
"tl": "tgl",
"tn": "tsn",
"to": "ton",
"tr": "tur",
"ts": "tso",
"tt": "tat",
"tw": "twi",
"ty": "tah",
"ug": "uig",
"uk": "ukr",
"ur": "urd",
"uz": "uzb",
"ve": "ven",
"vi": "vie",
"vo": "vol",
"wa": "wln",
"cy": "cym",
"wo": "wol",
"fy": "fry",
"xh": "xho",
"yi": "yid",
"yo": "yor",
"za": "zha",
"zu": "zul"
};
/**
* Tell whether or not the str does not start with a lower case ASCII char.
* @private
* @param {string} str the char to check
* @return {boolean} true if the char is not a lower case ASCII char
*/
Locale._notLower = function(str) {
// do this with ASCII only so we don't have to depend on the CType functions
var ch = str.charCodeAt(0);
return ch < 97 || ch > 122;
};
/**
* Tell whether or not the str does not start with an upper case ASCII char.
* @private
* @param {string} str the char to check
* @return {boolean} true if the char is a not an upper case ASCII char
*/
Locale._notUpper = function(str) {
// do this with ASCII only so we don't have to depend on the CType functions
var ch = str.charCodeAt(0);
return ch < 65 || ch > 90;
};
/**
* Tell whether or not the str does not start with a digit char.
* @private
* @param {string} str the char to check
* @return {boolean} true if the char is a not an upper case ASCII char
*/
Locale._notDigit = function(str) {
// do this with ASCII only so we don't have to depend on the CType functions
var ch = str.charCodeAt(0);
return ch < 48 || ch > 57;
};
/**
* Tell whether or not the given string has the correct syntax to be
* an ISO 639 language code.
*
* @private
* @param {string} str the string to parse
* @return {boolean} true if the string could syntactically be a language code.
*/
Locale._isLanguageCode = function(str) {
if (typeof(str) === 'undefined' || str.length < 2 || str.length > 3) {
return false;
}
for (var i = 0; i < str.length; i++) {
if (Locale._notLower(str.charAt(i))) {
return false;
}
}
return true;
};
/**
* Tell whether or not the given string has the correct syntax to be
* an ISO 3166 2-letter region code or M.49 3-digit region code.
*
* @private
* @param {string} str the string to parse
* @return {boolean} true if the string could syntactically be a language code.
*/
Locale._isRegionCode = function (str) {
if (typeof(str) === 'undefined' || str.length < 2 || str.length > 3) {
return false;
}
if (str.length === 2) {
for (var i = 0; i < str.length; i++) {
if (Locale._notUpper(str.charAt(i))) {
return false;
}
}
} else {
for (var i = 0; i < str.length; i++) {
if (Locale._notDigit(str.charAt(i))) {
return false;
}
}
}
return true;
};
/**
* Tell whether or not the given string has the correct syntax to be
* an ISO 639 language code.
*
* @private
* @param {string} str the string to parse
* @return {boolean} true if the string could syntactically be a language code.
*/
Locale._isScriptCode = function(str) {
if (typeof(str) === 'undefined' || str.length !== 4 || Locale._notUpper(str.charAt(0))) {
return false;
}
for (var i = 1; i < 4; i++) {
if (Locale._notLower(str.charAt(i))) {
return false;
}
}
return true;
};
/**
* Return the ISO-3166 alpha3 equivalent region code for the given ISO 3166 alpha2
* region code. If the given alpha2 code is not found, this function returns its
* argument unchanged.
* @static
* @param {string|undefined} alpha2 the alpha2 code to map
* @return {string|undefined} the alpha3 equivalent of the given alpha2 code, or the alpha2
* parameter if the alpha2 value is not found
*/
Locale.regionAlpha2ToAlpha3 = function(alpha2) {
return Locale.a2toa3regmap[alpha2] || alpha2;
};
/**
* Return the ISO-639 alpha3 equivalent language code for the given ISO 639 alpha1
* language code. If the given alpha1 code is not found, this function returns its
* argument unchanged.
* @static
* @param {string|undefined} alpha1 the alpha1 code to map
* @return {string|undefined} the alpha3 equivalent of the given alpha1 code, or the alpha1
* parameter if the alpha1 value is not found
*/
Locale.languageAlpha1ToAlpha3 = function(alpha1) {
return Locale.a1toa3langmap[alpha1] || alpha1;
};
Locale.prototype = {
/**
* @private
*/
_genSpec: function () {
this.spec = this.language || "";
if (this.script) {
if (this.spec.length > 0) {
this.spec += "-";
}
this.spec += this.script;
}
if (this.region) {
if (this.spec.length > 0) {
this.spec += "-";
}
this.spec += this.region;
}
if (this.variant) {
if (this.spec.length > 0) {
this.spec += "-";
}
this.spec += this.variant;
}
},
/**
* Return the ISO 639 language code for this locale.
* @return {string|undefined} the language code for this locale
*/
getLanguage: function() {
return this.language;
},
/**
* Return the language of this locale as an ISO-639-alpha3 language code
* @return {string|undefined} the alpha3 language code of this locale
*/
getLanguageAlpha3: function() {
return Locale.languageAlpha1ToAlpha3(this.language);
},
/**
* Return the ISO 3166 region code for this locale.
* @return {string|unde