/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/*jshint browser: true */
/*global define, niagara */
/**
* A JavaScript library used to access translated Lexicon values from the Niagara
* Framework.
*
* This library will make network calls back to a Web Server to access translated
* values.
*
* Attempts will also be made to use local storage to cache recorded Lexicon
* values. If a user logs on with a different locale or the registry has been updated,
* this storage will be automatically cleared.
*
* Please try out the examples from the `BajauxExamples` folder available from the
* `docDeveloper` palette to see how this gets used. Also there are some more code
* examples embedded into the method comments below.
*
* RequireJS configuration options can be specified for this library...
*
* - **noStorage**: if truthy, no attempt at using storage will be used.
* - **forceInit**: if truthy, an 'init' network call will be always be made
* the first time this library loads.
* - **lang**: if specified, the user locale for the Lexicons. If this
* isn't specified the library will make a network call
* for it when it first loads.
* - **storageId**: if specified, the id that helps determine whether
* the storage database currently being used is out of date.
*
* This library is designed to be used through the RequireJS Lexicon plugin...
*
* @example
* <caption>
* Access the Lexicon JS library using RequireJS.
* </caption>
* require(["lex!"], function (lexjs) {
* lexjs.module("js")
* .then(function (lex) {
* console.log("The Dialog OK button text: " + lex.get("dialogs.ok"));
* });
* });
*
* @example
* <caption>
* Directly access a module's Lexicon using the plug-in syntax for RequireJS
* </caption>
* require(["lex!js,bajaui"], function (lexicons) {
* // The lexicon's array holds the Lexicon for both the js and bajaui modules.
* console.log("The Dialog OK button text: " + lexicons[0].get("dialogs.ok"));
* });
*
* @module nmodule/js/rc/lex/lex
* @requires Promise, underscore, jquery
* @see {@link module:lex}
*/
define([
'module',
'Promise',
'underscore',
'jquery',
'nmodule/js/rc/rpc/rpc' ], function (
module,
Promise,
_,
$,
rpc) {
"use strict";
// RequireJS Config.
var config = module.config(),
// Database of stored Lexicons
lexicons = {},
// Cached promises so multiple network calls for the same
// Lexicon aren't made at the same time.
openModulePromises = {},
// Deferred promise used in initialization
initPromise,
Lexicon,
// User language
lang = config.lang,
// Storage is used to cache Lexicon information
storage,
storageId = config.storageId,
storageKeyName = "niagaraLex",
// Load the lexicons using Workbench.
wbutil,
exports,
// Used for writing out to localStorage on unload
isLocalStorageEmptyAtLoad = true;
// If available, attempt to use storage
if (window && !config.noStorage) {
try {
storage = window.localStorage;
} catch (ignore) {}
}
////////////////////////////////////////////////////////////////
// Lexicon
////////////////////////////////////////////////////////////////
/**
* A Lexicon is a map of locale specific name/value pairs for a module.
*
* An instance of a Lexicon can be accessed indirectly by use of the
* module method.
*
* @class
* @inner
* @public
*
* @param {String} moduleName The name of the Niagara Module this Lexicon relates too.
* @param {Object} data An object contained key values pairs for the Lexicon.
*
* @example
* <caption>Access a module's Lexicon</caption>
* lexjs.module("js")
* .then(function (lex) {
* console.log("Some text from a lexicon: " + lex.get("dialogs.ok"));
* });
*/
Lexicon = function Lexicon(moduleName, data) {
this.$moduleName = moduleName;
this.$data = data;
};
/**
* Return a value from the Lexicon for a given key.
*
* The argument for this method can be either a String key followed by arguments or an Object Literal.
*
* @param {Object|String} obj the Object Literal that contains the method's arguments or a String key.
* @param {String} obj.key the key to look up.
* @param {String} obj.def the default value to return if the key can't be found.
* By default this is null.
* @param {Array|String} obj.args arguments used for String formatting. If the first parameter
* is a String key, this list can just be further arguments for the function.
*
* @returns {String} the value for the Lexicon or return def if can't be found.
*
* @example
* <caption>Access a Lexicon value via its key name.</caption>
* lexjs.module("js")
* .then(function (lex) {
* console.log(lex.get("dialogs.ok"));
* });
*
* @example
* <caption>Access a Lexicon value via its key name with some formatted parameters.</caption>
* lexjs.module("bajaui")
* .then(function (lex) {
* var val = lex.get("fileSearch.scanningFiles", "alpha", "omega"))
* // Prints out: Scanning files (found alpha of omega)...
* console.log(val);
* }));
*
* @example
* <caption>Provide a default value if the key can't be found and use an Object Literal
* instead</caption>
* lexjs.module("bajaui")
* .then(function (lex) {
* // Use an Object Literal instead of multiple arguments and provide a default value.
* var val = lex.get({
* key: "fileSearch.scanningFiles",
* def: "Return this if the key can't be found in the Lexicon",
* args: ["alpha", "omega"]
* });
* console.log(val);
* });
*/
Lexicon.prototype.get = function get(obj) {
obj = obj && obj.constructor === Object ? obj : { key: obj };
obj.def = obj.def || "";
var val = this.$data[obj.key] || obj.def,
regex,
args = obj.args;
if (args || arguments.length > 1) {
args = args || [];
args = args.concat(Array.prototype.slice.call(arguments, 1));
// If we have some message formatting arguments them use them
if (args.length > 0) {
// Replace {number} with value from args
regex = /\{[0-9]+\}/g;
val = val.replace(regex, function (entry) {
var i = parseInt(entry.substring(1, entry.length - 1), 10);
return args[i] !== undefined ? args[i] : entry;
});
}
}
return val;
};
/**
* Return escaped value of the key which is safe to display.
* @see Lexicon.get
*
* @since Niagara 4.8
*
* @param {Object|String} obj the Object Literal that contains the method's arguments or a String key.
* @param {String} obj.key the key to look up.
* @param {String} obj.def the default value to return if the key can't be found.
* By default this is null.
* @param {Array|String} obj.args arguments used for String formatting. If the first parameter
* is a String key, this list can just be further arguments for the function.
*
* @returns {String}
*/
Lexicon.prototype.getSafe = function getSafe(obj) {
var raw = Lexicon.prototype.get.apply(this, arguments);
return _.escape(raw);
};
/**
* Return the raw and unescaped value of the key which is not safe to display.
* @see Lexicon.get
*
* @since Niagara 4.8
*
* @param {Object|String} obj the Object Literal that contains the method's arguments or a String key.
* @param {String} obj.key the key to look up.
* @param {String} obj.def the default value to return if the key can't be found.
* By default this is null.
* @param {Array|String} obj.args arguments used for String formatting. If the first parameter
* is a String key, this list can just be further arguments for the function.
* @returns {String}
*/
Lexicon.prototype.getRaw = function getRaw(obj) {
return Lexicon.prototype.get.apply(this, arguments);
};
/**
* Return the Lexicon's module name.
*
* @returns {String}
*
* @example
* <caption>Return a Lexicon's module name</caption>
* lexjs.module("bajaui")
* .then(function (lex) {
* // Prints out: bajaui
* console.log(lex.getModuleName());
* }));
*/
Lexicon.prototype.getModuleName = function getModuleName() {
return this.$moduleName;
};
////////////////////////////////////////////////////////////////
// Util
////////////////////////////////////////////////////////////////
function doRpc(methodName, args) {
var params = {
typeSpec: 'web:LexiconRpc',
methodName: methodName,
args: args
};
if (!config.noBajaScript && require.specified('baja')) {
return rpc.baja(params);
} else {
return rpc.ajax(params);
}
}
function checkWbEnv() {
wbutil = (
typeof niagara !== "undefined" &&
niagara.env &&
niagara.env.useLocalWbRc &&
niagara.wb &&
niagara.wb.util) ? niagara.wb.util : null;
}
checkWbEnv();
/**
* Unescape the string so all escaped characters become readable.
* Note: Any change to SlotPath.unescape needs to trickle in here as well.
* This copy is to avoid requiring baja.SlotPath here.
* @see baja.SlotPath.escape
*
* @param {String} str the string to be unescaped.
*
* @returns {String} the unescaped String.
*/
function unescape(str) {
if (str.length === 0) {
return str;
}
// Convert from $xx
str = str.replace(/\$[0-9a-fA-F]{2}/g, function (s) {
return String.fromCharCode(parseInt(s.substring(1, s.length), 16));
});
// Convert from $uxxxx
str = str.replace(/\$u[0-9a-fA-F]{4}/g, function (s) {
return String.fromCharCode(parseInt(s.substring(2, s.length), 16));
});
return str;
}
////////////////////////////////////////////////////////////////
// Storage
////////////////////////////////////////////////////////////////
/**
* If storage is available, clear the storage for the Lexicon database.
*/
function clear() {
if (storage) {
try {
storage.removeItem(storageKeyName);
} catch (ignore) {}
}
}
/**
* If storage is available and the library has fully initialized,
* load the Lexicons from the storage database.
*/
function load() {
var db;
if (storage && lang && storageId && !wbutil) {
try {
db = storage.getItem(storageKeyName);
if (db) {
isLocalStorageEmptyAtLoad = false;
db = JSON.parse(db);
if (db && db.modules) {
if (db.lang !== lang || db.storageId !== storageId) {
// If the language or registry database has changed, clear the storage since
// the db has potentially become out of date.
clear();
} else {
// Load the Lexicons from the storage database.
_.each(db.modules, function (data, moduleName) {
lexicons[moduleName] = new Lexicon(moduleName, data);
});
}
} else {
// If we have an invalid storage database then clear it.
clear();
}
}
} catch (e) {
// If any errors occur whilst trying to read the database then clear it.
clear();
}
}
}
/**
* If storage is available, attempt to save the Lexicons to the storage database.
*/
function save() {
if (storage && lang && storageId && !wbutil) {
if (!storage.getItem(storageKeyName) && !isLocalStorageEmptyAtLoad) {
return; // don't write since it is a cache clear operation
}
try {
// When saving the database, note down the language and last registry
// build time so we can test to see if the database is out of date
// next time we load it.
var db = {
lang: lang,
storageId: storageId,
modules: {}
};
_.each(lexicons, function (lex, key) {
db.modules[key] = lex.$data;
});
storage.setItem(storageKeyName, JSON.stringify(db));
} catch (e) {
// If there are any problems saving the storage then attempt to
// clear it.
clear();
}
}
}
// Try to automatically save the Lexicon database to local storage
if (window) {
$(window).on("pagehide unload", save);
}
////////////////////////////////////////////////////////////////
// Initialization
////////////////////////////////////////////////////////////////
/**
* Initialization happens when the library is first loaded.
* <p>
* If the last registry build time and language haven't been
* specified in the start up options, a network call will
* be made to request this information.
* <p>
* If the 'forceInit' configuration option is true, an 'init'
* network call will always be made.
*/
(function init() {
if (storageId && lang && !config.forceInit) {
initPromise = Promise.resolve(load());
} else {
initPromise = doRpc('init')
.then(function (data) {
lang = lang || data.lang;
storageId = data.storageId;
return load();
});
}
}());
////////////////////////////////////////////////////////////////
// Exports
////////////////////////////////////////////////////////////////
exports = {
/**
* Asynchronously resolve a Lexicon via module name. A promise is returned and resolved once
* Lexicon has been found. If the Lexicon can't be found or there's a network error,
* the promise will reject.
*
* @param {String} moduleName the name of the module being requested.
* @returns {Promise}
*
* @example
* <caption>Access a Lexicon via its module name</caption>
* lexjs.module("myModule")
* .then(function (lex) {
* // Access the Lexicon entry called 'foo' from 'myModule'
* console.log(lex.get("foo"));
* });
*/
module: function bajaModule(moduleName) {
var promise = openModulePromises[moduleName],
lexData;
if (!promise) {
promise = openModulePromises[moduleName] = initPromise
.then(function () {
if (lexicons.hasOwnProperty(moduleName)) {
return lexicons[moduleName];
} else if (wbutil && typeof wbutil.getLexicon === "function") {
lexData = wbutil.getLexicon(moduleName, lang);
if (lexData) {
// Cache the call but in Workbench mode the save and load never happens.
return (lexicons[moduleName] = new Lexicon(moduleName, JSON.parse(lexData)));
} else {
return Promise.reject(new Error("Lexicon module not found: " +
moduleName));
}
} else {
lexData = module.config()["lexicon/" + moduleName + "/" + lang];
if (lexData) {
// If the Lexicon is already defined in requirejs then use the data
// but don't bother trying to cache the data as we don't want this saved
// to storage.
return new Lexicon(moduleName, lexData);
} else {
// Request the Lexicon via an AJAX Call
return doRpc("getLexicon", [ moduleName, lang ])
.then(function (data) {
return (lexicons[moduleName] = new Lexicon(moduleName, data));
});
}
}
})
.finally(function () {
delete openModulePromises[moduleName];
});
}
return promise;
},
/**
* If the Lexicon is loaded and cached then return it. Otherwise return null.
* Please note, this will not result in any network calls.
*
* @param {String} moduleName The name of the module.
*
* @return {Lexicon} The Lexicon or null if it can't be found.
*/
getLexiconFromCache: function (moduleName) {
return lexicons[moduleName] || null;
},
/**
* Asynchronously format a String using Niagara's BFormat conventions.
*
* @param {String} str the string that contains the BFormat style text
* to use. The syntax should be `%lexicon(moduleName:keyName)%` or
* `%lexicon(moduleName:keyName:formatString1:formatString2)%`.
* @returns {Promise}
*
* @example
* lexjs.format("%lexicon(bajaui:dialog.ok)% and %lexicon(bajaui:menu.new.label)%")
* .then(function (str) {
* // Prints: "OK and New"
* console.log(str);
* });
* lexjs.format("%lexicon(bajaui:fileSearch.scanningFiles:arg1:arg2)%")
* .then(function (str) {
* // Prints: "Scanning files (found arg1 of arg2)..."
* console.log(str);
* });
*/
format: function format(str) {
var that = this,
regex = /\%lexicon\(([a-zA-Z0-9]+)\:([a-zA-Z0-9.\-_]+)((?:\:[a-zA-Z0-9\$(?:\s)*]+)*)\)\%/g,
res,
lexiconPromises = [];
// Asynchronously access the Lexicon's for each module
res = regex.exec(str);
while (res) {
// Build up a list of the promises used to access the modules
lexiconPromises.push(that.module(res[1]/*moduleName*/));
res = regex.exec(str);
}
// When we have all the Lexicons, process the result and resolve the String
return Promise.all(lexiconPromises)
.then(function (args) {
var i = 0;
// Now we have all the Lexicons make the String replace
str = str.replace(regex, function (match, module, key, fmtArgsStr) {
return args[i++].get({
key: key,
def: key,
args: _.chain(fmtArgsStr.substring(1).split(":"))
.map(function (fmtArg) {
return unescape(fmtArg);
})
.value()
});
});
return str;
});
},
/**
* A private API to reset the Lexicon. This may clear out cached data
* and reinitialize some internal variables. This should only be used
* by Tridium developers.
*
* @private
*/
$reset: function () {
clear();
lexicons = {};
openModulePromises = {};
checkWbEnv();
}
};
return exports;
});