UNPKG

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
/*< 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