/*
* Copyright (C) 2012-2013 DFKI GmbH
* Deutsches Forschungszentrum fuer Kuenstliche Intelligenz
* German Research Center for Artificial Intelligence
* http://www.dfki.de
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
define(['mmirf/constants', '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.Constants
* @requires mmir.CommonUtils
* @requires mmir.SemanticInterpreter
*
*/
function(
constants, 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);
/**
* JSON object containing the contents of a dictionary file - which are
* found in 'config/languages/<language>/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
* @memberOf LanguageManager#
*/
function doCheckExistsGrammar(lang) {
var langFiles = null;
var retValue = false;
if (lang != null) {
langFiles = commonUtils.listDir(constants.getLanguagePath() + lang);
if (langFiles != null) {
if (langFiles.indexOf(constants.getGrammarFileName()) > -1) {
retValue = true;
}
}
}
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();
}
/**
* 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;
}
var path = constants.getLanguagePath() + lang + "/" + constants.getSpeechConfigFileName();
loadFile({
async : false,
dataType : "json",
url : path,
success : function(data) {
if(logger.isVerbose()) logger.v("[LanguageManager] Success. " + data);
currentSpeechConfig = data;
if(logger.isVerbose()) logger.v("[LanguageManager] " + JSON.stringify(dictionary));
},
error : function(xhr, statusStr, error) {
logger.error("[LanguageManager] 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;
}
var path = constants.getLanguagePath() + lang + "/" + constants.getDictionaryFileName();
loadFile({
async : false,
dataType : "json",
url : path,
success : function(data) {
dictionary = data;
},
error : function(xhr, statusStr, error) {
logger.error("[LanguageManager] 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("[LanguageManager] No language argument specified: using language from configuration '" + appLang + "'.");
}
else {
appLang = constants.getLanguage();
if (appLang) {
lang = appLang;
logger.info("[LanguageManager] No language argument specified: using language from mmir.constants '" + appLang + "'.");
}
else {
if (languages.length > 0) {
appLang = this.determineLanguage(lang);
if(appLang){
lang = appLang;
logger.info("[LanguageManager] No language argument specified: used determinLanguage() for selecting language '" + appLang + "'.");
}
}
}//END: else(consts::lang)
}//END: else(config::lang)
if(!lang){
logger.warn("[LanguageManager.init] No language specified. And no language could be read from directory '" + constants.getLanguagePath() + "'.");
}
}//END: if(!lang && !currentLanguage)
// get all the languages/dictionaries by name
languages = commonUtils.listDir(constants.getLanguagePath());
if (logger.isDebug()) logger.debug("[LanguageManager] Found dictionaries for: " + JSON.stringify(languages));// debug
var defer = deferred();
loadDictionary(lang);
loadSpeechConfig(lang);
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(constants.getLanguagePath() + lang);
if (langFiles != null) {
if (langFiles.indexOf(constants.getDictionaryFileName()) > -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(constants.getLanguagePath() + lang);
if (langFiles != null) {
if (langFiles.indexOf(constants.getSpeechConfigFileName()) > -1) {
retValue = true;
}
}
}
return retValue;
},
/**
* If a JSON grammar file exists for the given language, 'true' is
* returned. Else the method returns 'false'.
*
* @function
* @returns {Boolean} True if a grammar exists for given language.
* @param {String}
* Language String, i.e.: en, de
* @public
*
* @memberOf mmir.LanguageManager.prototype
*/
existsGrammar : doCheckExistsGrammar,
/**
* 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 = constants.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 = constants.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 constants.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 constants.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 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];
}
//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(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';
}
}
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;
});