Source: manager/settings/languageManager.js


define(['mmirf/resources', 'mmirf/configurationManager', 'mmirf/commonUtils', 'mmirf/semanticInterpreter', 'mmirf/util/deferred', 'mmirf/util/loadFile', 'mmirf/logger', 'module'],

	/**
	 * A class for managing the language of the application. <br>
	 * It's purpose is to load the controllers and their views / partials and provide functions to find controllers or
	 * perform actions or helper-actions.
	 *
	 * This "class" is structured as a singleton - so that only one instance is in use.<br>
	 *
	 * @name LanguageManager
	 * @memberOf mmir
	 * @class
	 *
	 *
	 * @requires mmir.Resources
	 * @requires mmir.CommonUtils
	 * @requires mmir.SemanticInterpreter
	 *
	 */
	function(
			res, configurationManager, commonUtils, semanticInterpreter, deferred, loadFile, Logger, module
){
			//the next comment enables JSDoc2 to map all functions etc. to the correct class description
			/** @scope mmir.LanguageManager.prototype */

			/**
			 * Object containing the instance of the class
			 * {@link LanguageManager}
			 *
			 * @type Object
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			var instance = null;

			/**
			 * @private
			 * @type Logger
			 * @memberOf LanguageManager#
			 */
			var logger = Logger.create(module);

			/**
			 * @private
			 * @type LanguageManagerModuleConfig
			 * @memberOf LanguageManager#
			 */
			var _conf = module.config(module);

			/**
			 * JSON object containing the contents of a dictionary file - which are
			 * found in 'config/languages/&lt;language&gt;/dictionary.json'.
			 *
			 * @type JSON
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			var dictionary = null;

			/**
			 * A String holding the currently loaded language, e.g. "en".
			 *
			 * @type String
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			var currentLanguage = null;

			/**
			 * A JSON-Object holding the speech-configuration for the currently loaded language.
			 *
			 * @type JSON-Object
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			var currentSpeechConfig = null;

			/**
			 * An array of all available languages.
			 *
			 * @type Array
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			var languages = null;

			/**
			 * A keyword which can be used in views (ehtml) to display the current
			 * language.<br>
			 * If this keyword is used inside a view or partial, it is replaced by the
			 * current language string.
			 *
			 * @type String
			 * @private
			 * @example @localize('current_language')
			 *
			 * @memberOf LanguageManager#
			 */
			var keyword_current_language = 'current_language';

			/**
			 * Function to set a new language, but only, if the new language is
			 * different from the current language.
			 *
			 * @function
			 * @param {String}
			 *			lang The language of the dictionary which should be loaded.
			 * @returns {String} The (new) current language
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			function setLanguage(lang) {
				if ((lang) && (currentLanguage != lang)) {
					loadDictionary(lang);
					loadSpeechConfig(lang);
					requestGrammar(lang);
				}
				return currentLanguage;
			}

			/**
			 * @function
			 * @private
			 * @param {String} lang
			 *			Language String, i.e.: en, de
			 * @param {"source"|"bin"} [grammarType] OPTIONAL
			 *			only check grammar specifications ("source", i.e. JSON grammar),
			 *			or executable grammar ("bin", i.e. compiled grammar) existence
			 * @returns {Boolean|"source"|"bin"} TRUE if a grammar exists for given language
			 * 																			 (and if grammarType was given, when the
			 * 																			  existing grammar matches the grammar type)
			 * @memberOf LanguageManager#
			 */
			function doCheckExistsGrammar(lang, grammarType) {
				var langFiles = null;
				var retValue = false;

				if (lang) {

								//check for existence of JSON grammar
								if(!grammarType || grammarType === 'source'){
						langFiles = commonUtils.listDir(res.getLanguagePath() + lang);
						if (langFiles) {
							if (langFiles.indexOf(res.getGrammarFileUrl()) > -1) {
								retValue = true;
							}
						}
								}

								//check for existence of compiled grammar
								if(!langFiles || !retValue && (!grammarType || grammarType === 'bin')){

									langFiles =	commonUtils.listDir(res.getGeneratedGrammarsPath().replace(/\/$/, ''));
									if(langFiles){
										var re = new RegExp(
															typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD?
																	'^mmirf/grammar/'+lang+'.js$' :
																	'^'+lang+'_'+res.getGrammarFileUrl().replace(/\.json/i, '.js')+'$',
															'i'
										);
										for(var i=langFiles.length - 1; i >= 0; --i){
											if(re.test(langFiles[i])){
												retValue = true;
												break;
											}
										}
									}

								}
				}
				return retValue;
			}

			/**
			 * Request grammar for the provided language.
			 *
			 * If there is no grammar available for the requested language, no new
			 * grammar is set.
			 *
			 * A grammar is available, if at least one of the following is true for the
			 * requested language
			 * <ul>
			 * <li>there exists a JSON grammar file (with correct name and at the
			 * 		correct location)</li>
			 * <li>there exists a compiled JavaScript grammar file (with correct name
			 * 		and at the correct location)</li>
			 * </ul>
			 *
			 * TODO document location for JSON and JavaScript grammar files
			 *
			 * @function
			 * @param {String}
			 *			lang The language of the grammar which should be loaded.
			 * @returns {String} The current grammar language
			 * @async
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			function requestGrammar(lang, doSetNextBestAlternative) {

				if (semanticInterpreter.hasGrammar(lang) || doCheckExistsGrammar(lang)) {
					semanticInterpreter.setCurrentGrammar(lang);
					return lang;
				}
				else if (doSetNextBestAlternative) {
					// try to find a language, for which a grammar is available
					var grammarLang = null;
					if (languages.length > 0) {
						// first: try to find a language with COMPILED grammar
						for ( var i = 0, size = languages.length; i < size; ++i) {
							grammarLang = languages[i];
							if (semanticInterpreter.hasGrammar(grammarLang)) {
								break;
							}
							else {
								grammarLang = null;
							}
						}

						// ... otherwise: try to find a language with JSON grammar:
						if (!grammarLang) {
							for ( var i = 0, size = languages.length; i < size; ++i) {
								grammarLang = languages[i];
								if (doCheckExistsGrammar(grammarLang)) {
									break;
								}
								else {
									grammarLang = null;
								}
							}
						}
					}

					if (grammarLang) {
						logger.warn('Could not find grammar for selected language ' + lang + ', using grammar for language ' + grammarLang + ' instead.');
						semanticInterpreter.setCurrentGrammar(grammarLang);
					}
					else {
						logger.info('Could not find any grammar for one of [' + languages.join(', ') + '], disabling SemanticInterpret.');
						semanticInterpreter.setEnabled(false);
					}
				}

				return semanticInterpreter.getCurrentGrammar();
			}

				/**
				 *
				 * @param  {"dictionary" | "speechConfig" | "grammar"} type the type of the resource
				 * @param  {String} lang the language code / ID
				 * @return {String} the resource ID for loading
				 */
				function getResourceUri(type, lang){
					if(typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD){
						// webpack ID: 'mmirf/settings/(dictionary|speechConfig|grammar)/<lang>'
						var id = 'mmirf/settings/'+type.replace(/Config$/, '')+'/'+lang;
						if(__webpack_modules__[id]){
							return __webpack_require__(id);
						}
					}
					var funcName = 'get' + type[0].toUpperCase() + type.substring(1) + 'FileUrl';
					return res[funcName](lang);
				}

			/**
			 * Loads the speech-configuration for the provided language and updates the current
			 * language.
			 *
			 * @function
			 * @param {String} lang
			 *			The language of the speech-configuration which should be loaded.
			 * @returns {String} The (new) current language
			 * @async
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			function loadSpeechConfig(lang) {

				if (lang && currentLanguage != lang) {
					currentLanguage = lang;
				}

						if(_conf && _conf.speech && _conf.speech[lang]){
							if(logger.isVerbose()) logger.verbose("loadSpeechConfig(): loaded configuration from module.config().speech["+lang+"] -> ", _conf.speech[lang]);
							currentSpeechConfig = _conf.speech[lang];
							return;/////////// EARLY EXIT ///////////////
						}

				var path = getResourceUri('speechConfig', lang);
				loadFile({
					async : false,
					dataType : "json",
					url : path,
					success : function(data) {

						if(logger.isVerbose()) logger.v("loadSpeechConfig("+lang+"): success -> ", data);

						currentSpeechConfig = data;
					},
					error : function(_xhr, _statusStr, error) {
						logger.error("loadSpeechConfig("+lang+"): Error loading speech configuration from \""+path+"\": " + error? error.stack? error.stack : error : ''); // error
					}
				});
				return currentLanguage;
			}

			/**
			 * Loads the dictionary for the provided language and updates the current
			 * language.
			 *
			 * @function
			 * @param {String}
			 *			lang The language of the dictionary which should be loaded.
			 * @returns {String} The (new) current language
			 * @async
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			function loadDictionary(lang) {

				if (lang && currentLanguage != lang) {
					currentLanguage = lang;
				}

						if(_conf && _conf.dictionary && _conf.dictionary[lang]){
							if(logger.isVerbose()) logger.verbose("loadDictionary(): loaded configuration from module.config().dictionary["+lang+"] -> ", _conf.dictionary[lang]);
							dictionary = _conf.dictionary[lang];
							return;/////////// EARLY EXIT ///////////////
						}

				var path = getResourceUri('dictionary', lang);
				loadFile({
					async : false,
					dataType : "json",
					url : path,
					success : function(data) {
							if(logger.isVerbose()) logger.v("loadDictionary("+lang+"): success -> ", data);
										dictionary = data;
					},
					error : function(_xhr, _statusStr, error) {
						logger.error("loadDictionary("+lang+"): Error loading language dictionary from \""+path+"\": " + error? error.stack? error.stack : error : ''); // error
					}
				});
				return currentLanguage;
			}
			/**
			 * Translates a keyword using the current dictionary and returns the
			 * translation.
			 *
			 * @function
			 * @param {String}
			 *			textVarName The keyword which should be looked up
			 * @returns {String} the translation of the keyword
			 * @private
			 *
			 * @memberOf LanguageManager#
			 */
			function internalGetText(textVarName) {
				var translated = "";
				if (dictionary[textVarName] && dictionary[textVarName].length > 0) {
					translated = dictionary[textVarName];
				}
				else if (textVarName === keyword_current_language){
					translated = currentLanguage;
				}
				else {
					translated = "undefined";
					logger.warn("[Dictionary] '" + textVarName + "' not found in " + JSON.stringify(dictionary));
				}
				return translated;
			}

			/**
			 * Constructor-Method of Singleton mmir.LanguageManager.<br>
			 *
			 * @constructs LanguageManager
			 * @memberOf LanguageManager#
			 * @private
			 * @ignore
			 *
			 */
			function constructor() {

				var _isInitialized = false;

				/** @lends mmir.LanguageManager.prototype */
				return {

					/**
					 * @param {String} [lang] OPTIONAL
					 * 				a language code for setting the current language and
					 * 				selecting the corresponding language resources
					 *
					 * @returns {Promise}
					 * 				a deferred promise that gets resolved when the language manager is initialized
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					init: function(lang){

						if (!lang && !currentLanguage) {

							//try to retrieve language from configuration:
							var appLang = configurationManager.get("language");
							if (appLang) {

								lang = appLang;
								logger.info("init(): No language argument specified: using language from configuration '" + appLang + "'.");

							}
							else {

								appLang = res.getLanguage();

								if (appLang) {

									lang = appLang;
									logger.info("init(): No language argument specified: using language from mmir.res '" + appLang + "'.");
								}
								else {

									if (languages.length > 0) {

										appLang = this.determineLanguage(lang);
										if(appLang){

											lang = appLang;

											logger.info("init() No language argument specified: used determinLanguage() for selecting language '" + appLang + "'.");
										}
									}

								}//END: else(consts::lang)

							}//END: else(config::lang)

							if(!lang){
								logger.warn("init(): No language specified. And no language could be read from directory '" + res.getLanguagePath() + "'.");
							}

						}//END: if(!lang && !currentLanguage)


						// get all the languages/dictionaries by name
						languages = commonUtils.listDir(res.getLanguagePath()) || [];

						if (logger.isDebug()) logger.debug("init() Found dictionaries for: " + JSON.stringify(languages));

						var defer = deferred();

						if(this.existsDictionary(lang)){
							loadDictionary(lang);
						} else if(logger.isDebug()){
							logger.debug("init(): no dictionary for language " + lang);
						}

						if(this.existsSpeechConfig(lang)){
							loadSpeechConfig(lang);
						} else if(logger.isDebug()){//TODO set/generate default speech-config?
							logger.debug("init(): no speech-config (asr/tts) for language " + lang);
						}

						//DISABLED semanticInterpreter may not be initialized yet / grammars loaded yet -> do this in main-initialization (see main.js)
						// requestGrammar(lang, true);//2nd argument TRUE: if no grammar is available for lang, try to find/set any available grammar

						_isInitialized = true;
						defer.resolve(this);

						return defer;
					},

					/**
					 * Returns the dictionary of the currently used language.
					 *
					 * @function
					 * @returns {Object} The JSON object for the dictionary of the
					 *		  currently used language
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					getDictionary : function() {
						return dictionary;
					},

					/**
					 * If a dictionary exists for the given language, 'true' is
					 * returned. Else the method returns 'false'.
					 *
					 * @function
					 * @returns {Boolean} True if a dictionary exists for given
					 *		  language.
					 * @param {String}
					 *			Language String, i.e.: en, de
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					existsDictionary : function(lang) {
						var langFiles = null;
						var retValue = false;

						if (lang != null) {
							langFiles = commonUtils.listDir(res.getLanguagePath() + lang);
							if (langFiles != null) {
								if (langFiles.indexOf(res.getDictionaryFileUrl()) > -1) {
									retValue = true;
								}
							}
						}
						return retValue;
					},

					/**
					 * If a speech-configuration (file) exists for the given language.
					 *
					 * @function
					 * @returns {Boolean}
					 * 				<code>true</code>if a speech-configuration exists for given language.
					 * 				Otherwise <code>false</code>.
					 *
					 * @param {String} lang
					 *			the language for which existence of the configuration should be checked, e.g. en, de
					 *
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					existsSpeechConfig : function(lang) {
						var langFiles = null;
						var retValue = false;

						if (lang != null) {
							langFiles = commonUtils.listDir(res.getLanguagePath() + lang);
							if (langFiles != null) {
								if (langFiles.indexOf(res.getSpeechConfigFileUrl()) > -1) {
									retValue = true;
								}
							}
						}
						return retValue;
					},

					/**
					 * If a JSON grammar file exists for the given language, 'true' is
					 * returned. Else the method returns 'false'.
					 *
					 * @function
					 * @param {String} lang
					 *			Language String, i.e.: en, de
					 * @param {"source"|"bin"} [grammarType] OPTIONAL
					 *			only check grammar specifications ("source", i.e. JSON grammar),
					 *			or executable grammar ("bin", i.e. compiled grammar) existence
					 * @returns {Boolean|"source"|"bin"} TRUE if a grammar exists for given language
					 * 																			 (and if grammarType was given, when the
					 * 																			  existing grammar matches the grammar type)
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					existsGrammar : doCheckExistsGrammar,
					/**
					 * @borrows #requestGrammar
					 * @memberOf mmir.LanguageManager.prototype
					 */
					_requestGrammar : requestGrammar,

					/**
					 * Chooses a language for the application.
					 *
					 * <p>
					 * The language selection is done as follows:
					 * <ol>
					 * <li>check if a default language exists<br>
					 * if it does and if both (!) grammar and dictionary exist for this
					 * language, return this language </li>
					 * <li>walk through all languages alphabetically
					 * <ol>
					 * <li>if for a language both (!) grammar and dictionary exist,
					 * return this language memorize the first language with a grammar
					 * (do not care, if a dictionary exists) </li>
					 * </ol>
					 * <li>test if a grammar exists for the default language - do not
					 * care about dictionaries - if it does, return the default language
					 * </li>
					 * <li>If a language was found (in Step 2.1) return this language
					 * </li>
					 * <li>If still no language is returned take the default language
					 * if it has a dictionary </li>
					 * <li>If a language exists, take it (the first one) </li>
					 * <li>Take the default language - no matter what </li>
					 * </ol>
					 *
					 * @function
					 * @returns {String} The determined language
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					determineLanguage : function(lang) {
						var tempLanguage = lang;
						var firstLanguageWithGrammar = null;

						// first check, if language - given in parameter - exists
						if (tempLanguage != null) {
							// check if both grammar and dictionary exist for given
							// language
							if (instance.existsGrammar(tempLanguage) && instance.existsDictionary(tempLanguage)) {
								return tempLanguage;
							}
						}

						tempLanguage = res.getLanguage();
						// then check, if default language exists
						if (tempLanguage != null) {
							// check if both grammar and dictionary exist for default
							// language
							if (instance.existsGrammar(tempLanguage) && instance.existsDictionary(tempLanguage)) {
								return tempLanguage;
							}
						}
						// walk through the languages alphabetically
						for ( var i = 0; i < languages.length; i++) {
							tempLanguage = languages[i];
							// check if a grammar and dictionary exists for every
							// language
							if (instance.existsGrammar(tempLanguage)) {

								// memorize the first language with a grammar (for
								// later)
								if (firstLanguageWithGrammar == null) {
									firstLanguageWithGrammar = tempLanguage;
								}

								if (instance.existsDictionary(tempLanguage)) {
									return tempLanguage;
								}
							}
						}

						// still no language found - take the default language and test
						// if a grammar exists
						tempLanguage = res.getLanguage();
						if (tempLanguage != null) {
							// check if both grammar and dictionary exist for default
							// language
							if (instance.existsGrammar(tempLanguage)) {
								return tempLanguage;
							} else if (firstLanguageWithGrammar != null) {
								return firstLanguageWithGrammar;
							} else if (instance.existsDictionary(tempLanguage)) {
								return tempLanguage;
							}
						}

						// still no language - take the first one
						tempLanguage = languages[0];
						if (tempLanguage != null) {
							return tempLanguage;
						}

						return res.getLanguage();
					},

					/**
					 * Sets a new language, but only, if the new language is different
					 * from the current language.
					 *
					 * @function
					 * @returns {String} The (new) current language
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					setLanguage : function(lang) {
						return setLanguage(lang);
					},

					/**
					 * Gets the language currently used for the translation.
					 *
					 * @function
					 * @returns {String} The current language
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					getLanguage : function() {
						return currentLanguage;
					},

					/**
					 * Gets the default language.
					 *
					 * @function
					 * @returns {String} The default language
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					getDefaultLanguage : function() {
						return res.getLanguage();
					},

					/**
					 * Gets an array of all for the translation available languages.<br>
					 *
					 * @function
					 * @returns {String} An array of all for the translation available
					 *		  languages
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					getLanguages : function() {
						return languages;
					},

					/**
					 * Looks up a keyword in the current dictionary and returns the
					 * translation.
					 *
					 * @function
					 * @param {String}
					 *			textVarName The keyword which is to be translated
					 * @returns {String} The translation of the keyword
					 * @public
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					getText : function(textVarName) {
						return internalGetText(textVarName);
					},

					/**
					 * Get the language code setting for a specific plugin.
					 *
					 * Returns the default setting, if no specific setting for the specified plugin was defined.
					 *
					 * @public
					 * @param {String} pluginId
					 * @param {String|Array<String>} [feature] OPTIONAL
					 * 				dot-separate path String or "path Array"
					 * 				This parameter may be omitted, if no <code>separator</code> parameter
					 * 				is used.
					 * 				DEFAULT: "language" (the language feature)
					 * @param {String} [separator] OPTIONAL
					 * 				the speparator-string that should be used for separating
					 * 				the country-part and the language-part of the code
					 * @returns {String} the language-setting/-code
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					getLanguageConfig : function(pluginId, feature, separator) {

									if(!currentSpeechConfig){
										logger.warn('no speech configuration ('+res.getSpeechConfigFileUrl()+') available for '+currentLanguage);
										if(!feature || feature === 'language' || feature === 'long'){
											return currentLanguage;
										}
										return void(0);
									}

						//if nothing is specfied:
						//	return default language-setting
						if(typeof pluginId === 'undefined'){
							return currentSpeechConfig.language; /////////// EARLY EXIT ///////////////
						}

						//ASSERT pluginId is defined

						//default feature is language
						if(typeof feature === 'undefined'){
							feature = 'language';
						}

						var value;
						if(currentSpeechConfig.plugins && currentSpeechConfig.plugins[pluginId] && typeof currentSpeechConfig.plugins[pluginId][feature] !== 'undefined'){
							//if there is a plugin-specific setting for this feature
							value = currentSpeechConfig.plugins[pluginId][feature];
						}
						else if(feature !== 'plugins' && typeof currentSpeechConfig[feature] !== 'undefined'){
							//otherwise take the default setting (NOTE: the name "plugins" is not allowed for features!)
							value = currentSpeechConfig[feature];
						}

						//fallback: if long language code was requested but neither plugin nor global long feature is available -> try to return language
						if(typeof value === 'undefined' && feature === 'long'){
							return this.getLanguageConfig(pluginId, 'language', separator);
						}

						//if there is a separator specified: replace default separator '-' with this one
						if(value && typeof separator !== 'undefined'){
							value = value.replace(/-/, separator);
						}

						return value;
					},

					/**
					 * HELPER for dealing with specific language / country code quirks of speech services:
					 * Get the language code for a specific ASR or TTS service, that is if the service requires some
					 * specific codes/transformations, then the transformed code is retruned by this function
					 * (otherwise the unchanged langCode is returned).
					 *
					 * @public
					 * @param {String} providerName
					 * 					corrections for: "nuance" | "mary"
					 * @param {String} langCode
					 * 					the original language / country code
					 * @returns {String} the (possibly "fixed") language-setting/-code
					 *
					 * @memberOf mmir.LanguageManager.prototype
					 */
					fixLang : function(providerName, langCode) {

						if(!langCode){
							return langCode;
						}

						if(providerName === 'nuance'){

							//QUIRK nuanceasr short-language code for the UK is UK instead of GB:
							//  replace en-GB with en-UK if necessary (preserving separator char)
							langCode = langCode.replace(/en(.)GB/i, 'en$1UK');

						} else if(providerName === 'mary'){

							//QUIRK marytts does not accept language+country code for German:
							//  must only be language code
							if(/de.DE/i.test(langCode)){
								langCode = 'de';
							}
						} else if(providerName === 'google' && langCode && langCode.length === 7){

							//convert 3-letter codes to 2-letter codes for Google services
							var m = /(\w\w)\w.(\w\w)\w/.exec(langCode);
							if(m){
								langCode = m[1] + '-' + m[2];
							}
						}

						return langCode;
					}

				};//END: return{}


			}//END: construcor = function(){...


			//FIXME as of now, the LanguageManager needs to be initialized,
			//		by calling init()
			//		-> should this be done explicitly (async-loading for dictionary and grammar? with returned Deferred?)
			instance = new constructor();

			return instance;

});