1 /* 2 * Copyright (C) 2012-2013 DFKI GmbH 3 * Deutsches Forschungszentrum fuer Kuenstliche Intelligenz 4 * German Research Center for Artificial Intelligence 5 * http://www.dfki.de 6 * 7 * Permission is hereby granted, free of charge, to any person obtaining a 8 * copy of this software and associated documentation files (the 9 * "Software"), to deal in the Software without restriction, including 10 * without limitation the rights to use, copy, modify, merge, publish, 11 * distribute, sublicense, and/or sell copies of the Software, and to 12 * permit persons to whom the Software is furnished to do so, subject to 13 * the following conditions: 14 * 15 * The above copyright notice and this permission notice shall be included 16 * in all copies or substantial portions of the Software. 17 * 18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 */ 26 27 28 define(['constants', 'configurationManager', 'commonUtils', 'semanticInterpreter', 'logger', 'module'], 29 30 /** 31 * A class for managing the language of the application. <br> 32 * It's purpose is to load the controllers and their views / partials and provide functions to find controllers or 33 * perform actions or helper-actions. 34 * 35 * This "class" is structured as a singleton - so that only one instance is in use.<br> 36 * 37 * @name LanguageManager 38 * @memberOf mmir 39 * @class 40 * 41 * 42 * @requires mmir.Constants 43 * @requires mmir.CommonUtils 44 * @requires mmir.SemanticInterpreter 45 * 46 * 47 * @requires jQuery.Deferred 48 * @requires jQuery.ajax 49 * 50 */ 51 function( 52 constants, configurationManager, commonUtils, semanticInterpreter, Logger, module 53 ){ 54 //the next comment enables JSDoc2 to map all functions etc. to the correct class description 55 /** @scope mmir.LanguageManager.prototype */ 56 57 /** 58 * Object containing the instance of the class 59 * {@link LanguageManager} 60 * 61 * @type Object 62 * @private 63 * 64 * @memberOf LanguageManager# 65 */ 66 var instance = null; 67 68 /** 69 * @private 70 * @type Logger 71 * @memberOf LanguageManager# 72 */ 73 var logger = Logger.create(module); 74 75 /** 76 * JSON object containing the contents of a dictionary file - which are 77 * found in 'config/languages/<language>/dictionary.json'. 78 * 79 * @type JSON 80 * @private 81 * 82 * @memberOf LanguageManager# 83 */ 84 var dictionary = null; 85 86 /** 87 * A String holding the currently loaded language, e.g. "en". 88 * 89 * @type String 90 * @private 91 * 92 * @memberOf LanguageManager# 93 */ 94 var currentLanguage = null; 95 96 /** 97 * A JSON-Object holding the speech-configuration for the currently loaded language. 98 * 99 * @type JSON-Object 100 * @private 101 * 102 * @memberOf LanguageManager# 103 */ 104 var currentSpeechConfig = null; 105 106 /** 107 * An array of all available languages. 108 * 109 * @type Array 110 * @private 111 * 112 * @memberOf LanguageManager# 113 */ 114 var languages = null; 115 116 /** 117 * A keyword which can be used in views (ehtml) to display the current 118 * language.<br> 119 * If this keyword is used inside a view or partial, it is replaced by the 120 * current language string. 121 * 122 * @type String 123 * @private 124 * @example @localize('current_language') 125 * 126 * @memberOf LanguageManager# 127 */ 128 var keyword_current_language = 'current_language'; 129 130 /** 131 * Function to set a new language, but only, if the new language is 132 * different from the current language. 133 * 134 * @function 135 * @param {String} 136 * lang The language of the dictionary which should be loaded. 137 * @returns {String} The (new) current language 138 * @private 139 * 140 * @memberOf LanguageManager# 141 */ 142 function setLanguage(lang) { 143 if ((lang) && (currentLanguage != lang)) { 144 loadDictionary(lang); 145 loadSpeechConfig(lang); 146 requestGrammar(lang); 147 } 148 return currentLanguage; 149 } 150 151 /** 152 * @function 153 * @memberOf LanguageManager# 154 */ 155 function doCheckExistsGrammar(lang) { 156 var langFiles = null; 157 var retValue = false; 158 159 if (lang != null) { 160 langFiles = commonUtils.getDirectoryContents(constants.getLanguagePath() + lang); 161 if (langFiles != null) { 162 if (langFiles.indexOf(constants.getGrammarFileName()) > -1) { 163 retValue = true; 164 } 165 } 166 } 167 return retValue; 168 } 169 170 /** 171 * Request grammar for the provided language. 172 * 173 * If there is no grammar available for the requested language, no new 174 * grammar is set. 175 * 176 * A grammar is available, if at least one of the following is true for the 177 * requested language 178 * <ul> 179 * <li>there exists a JSON grammar file (with correct name and at the 180 * correct location)</li> 181 * <li>there exists a compiled JavaScript grammar file (with correct name 182 * and at the correct location)</li> 183 * </ul> 184 * 185 * TODO document location for JSON and JavaScript grammar files 186 * 187 * @function 188 * @param {String} 189 * lang The language of the grammar which should be loaded. 190 * @returns {String} The current grammar language 191 * @async 192 * @private 193 * 194 * @memberOf LanguageManager# 195 */ 196 function requestGrammar(lang, doSetNextBestAlternative) { 197 198 if (semanticInterpreter.hasGrammar(lang) || doCheckExistsGrammar(lang)) { 199 semanticInterpreter.setCurrentGrammar(lang); 200 return lang; 201 } 202 else if (doSetNextBestAlternative) { 203 // try to find a language, for which a grammar is available 204 var grammarLang = null; 205 if (languages.length > 0) { 206 // first: try to find a language with COMPILED grammar 207 for ( var i = 0, size = languages.length; i < size; ++i) { 208 grammarLang = languages[i]; 209 if (semanticInterpreter.hasGrammar(grammarLang)) { 210 break; 211 } 212 else { 213 grammarLang = null; 214 } 215 } 216 217 // ... otherwise: try to find a language with JSON grammar: 218 if (!grammarLang) { 219 for ( var i = 0, size = languages.length; i < size; ++i) { 220 grammarLang = languages[i]; 221 if (doCheckExistsGrammar(grammarLang)) { 222 break; 223 } 224 else { 225 grammarLang = null; 226 } 227 } 228 } 229 } 230 231 if (grammarLang) { 232 logger.warn('Could not find grammar for selected language ' + lang + ', using grammar for language ' + grammarLang + ' instead.'); 233 semanticInterpreter.setCurrentGrammar(grammarLang); 234 } 235 else { 236 logger.warn('Could not find any grammar for one of [' + languages.join(', ') + '], disabling SemanticInterpret.'); 237 semanticInterpreter.setEnabled(false); 238 } 239 } 240 241 return semanticInterpreter.getCurrentGrammar(); 242 } 243 /** 244 * Loads the speech-configuration for the provided language and updates the current 245 * language. 246 * 247 * @function 248 * @param {String} lang 249 * The language of the speech-configuration which should be loaded. 250 * @returns {String} The (new) current language 251 * @async 252 * @private 253 * 254 * @memberOf LanguageManager# 255 */ 256 function loadSpeechConfig(lang) { 257 258 if (lang && currentLanguage != lang) { 259 currentLanguage = lang; 260 } 261 var path = constants.getLanguagePath() + lang + "/" + constants.getSpeechConfigFileName(); 262 $.ajax({ 263 async : false, 264 dataType : "json", 265 url : path, 266 success : function(data) { 267 268 if(logger.isVerbose()) logger.v("[LanguageManager] Success. " + data); 269 270 currentSpeechConfig = data;// jQuery.parseJSON(data); 271 272 if(logger.isVerbose()) logger.v("[LanguageManager] " + JSON.stringify(dictionary)); 273 }, 274 error : function(xhr, statusStr, error) { 275 logger.error("[LanguageManager] Error loading speech configuration from \""+path+"\": " + error? error.stack? error.stack : error : ''); // error 276 } 277 }); 278 return currentLanguage; 279 } 280 281 /** 282 * Loads the dictionary for the provided language and updates the current 283 * language. 284 * 285 * @function 286 * @param {String} 287 * lang The language of the dictionary which should be loaded. 288 * @returns {String} The (new) current language 289 * @async 290 * @private 291 * 292 * @memberOf LanguageManager# 293 */ 294 function loadDictionary(lang) { 295 296 if (lang && currentLanguage != lang) { 297 currentLanguage = lang; 298 } 299 var path = constants.getLanguagePath() + lang + "/" + constants.getDictionaryFileName(); 300 $.ajax({ 301 async : false, 302 dataType : "json", 303 url : path, 304 success : function(data) { 305 dictionary = data; 306 }, 307 error : function(xhr, statusStr, error) { 308 logger.error("[LanguageManager] Error loading language dictionary from \""+path+"\": " + error? error.stack? error.stack : error : ''); // error 309 } 310 }); 311 return currentLanguage; 312 } 313 /** 314 * Translates a keyword using the current dictionary and returns the 315 * translation. 316 * 317 * @function 318 * @param {String} 319 * textVarName The keyword which should be looked up 320 * @returns {String} the translation of the keyword 321 * @private 322 * 323 * @memberOf LanguageManager# 324 */ 325 function internalGetText(textVarName) { 326 var translated = ""; 327 if (dictionary[textVarName] && dictionary[textVarName].length > 0) { 328 translated = dictionary[textVarName]; 329 } 330 else if (textVarName === keyword_current_language){ 331 translated = currentLanguage; 332 } 333 else { 334 translated = "undefined"; 335 logger.warn("[Dictionary] '" + textVarName + "' not found in " + JSON.stringify(dictionary)); 336 } 337 return translated; 338 } 339 340 /** 341 * Constructor-Method of Singleton mmir.LanguageManager.<br> 342 * 343 * @constructs LanguageManager 344 * @memberOf LanguageManager# 345 * @private 346 * @ignore 347 * 348 */ 349 function constructor() { 350 351 var _isInitialized = false; 352 353 /** @lends LanguageManager.prototype */ 354 return { 355 356 /** 357 * @param {String} [lang] OPTIONAL 358 * a language code for setting the current language and 359 * selecting the corresponding language resources 360 * 361 * @memberOf LanguageManager.prototype 362 */ 363 init: function(lang){ 364 365 if (!lang && !currentLanguage) { 366 367 //try to retrieve language from configuration: 368 var appLang = configurationManager.get("language"); 369 if (appLang) { 370 371 lang = appLang; 372 logger.info("[LanguageManager] No language argument specified: using language from configuration '" + appLang + "'."); 373 374 } 375 else { 376 377 appLang = constants.getLanguage(); 378 379 if (appLang) { 380 381 lang = appLang; 382 logger.info("[LanguageManager] No language argument specified: using language from mmir.constants '" + appLang + "'."); 383 } 384 else { 385 386 if (languages.length > 0) { 387 388 appLang = this.determineLanguage(lang); 389 if(appLang){ 390 391 lang = appLang; 392 393 logger.info("[LanguageManager] No language argument specified: used determinLanguage() for selecting language '" + appLang + "'."); 394 } 395 } 396 397 }//END: else(consts::lang) 398 399 }//END: else(config::lang) 400 401 if(!lang){ 402 logger.warn("[LanguageManager.init] No language specified. And no language could be read from directory '" + constants.getLanguagePath() + "'."); 403 } 404 405 }//END: if(!lang && !currentLanguage) 406 407 408 // get all the languages/dictionaries by name 409 languages = commonUtils.getDirectoryContents(constants.getLanguagePath()); 410 411 if (logger.isDebug()) logger.debug("[LanguageManager] Found dictionaries for: " + JSON.stringify(languages));// debug 412 413 loadDictionary(lang); 414 loadSpeechConfig(lang); 415 requestGrammar(lang, true);//2nd argument TRUE: if no grammar is available for lang, try to find/set any available grammar 416 417 _isInitialized = true; 418 419 return this; 420 }, 421 /** 422 * Initializes the LanguageManager instance if necessary, and sets the Language to lang. 423 * 424 * If no language is supplied as parameter, then the property *language* 425 * from {@link mmir.Configuration} is used or the first language found 426 * in the language directory. 427 * 428 * @deprecated instead use LanguageManager directly (NOTE: before starting to use LanguageManager, init() has to be invoked once) 429 * 430 * @param {String} [lang] OPTIONAL 431 * The language which should be used throughout the 432 * application. 433 */ 434 getInstance: function(lang){ 435 436 if(_isInitialized === false){ 437 _isInitialized = true; 438 this.init(lang); 439 } 440 else if(lang) { 441 setLanguage(lang); 442 } 443 444 return this; 445 }, 446 447 /** 448 * Returns the dictionary of the currently used language. 449 * 450 * @function 451 * @returns {Object} The JSON object for the dictionary of the 452 * currently used language 453 * @public 454 */ 455 getDictionary : function() { 456 return dictionary; 457 }, 458 459 /** 460 * If a dictionary exists for the given language, 'true' is 461 * returned. Else the method returns 'false'. 462 * 463 * @function 464 * @returns {Boolean} True if a dictionary exists for given 465 * language. 466 * @param {String} 467 * Language String, i.e.: en, de 468 * @public 469 */ 470 existsDictionary : function(lang) { 471 var langFiles = null; 472 var retValue = false; 473 474 if (lang != null) { 475 langFiles = commonUtils.getDirectoryContents(constants.getLanguagePath() + lang); 476 if (langFiles != null) { 477 if (langFiles.indexOf(constants.getDictionaryFileName()) > -1) { 478 retValue = true; 479 } 480 } 481 } 482 return retValue; 483 }, 484 485 /** 486 * If a speech-configuration (file) exists for the given language. 487 * 488 * @function 489 * @returns {Boolean} 490 * <code>true</code>if a speech-configuration exists for given language. 491 * Otherwise <code>false</code>. 492 * 493 * @param {String} lang 494 * the language for which existence of the configuration should be checked, e.g. en, de 495 * 496 * @public 497 */ 498 existsSpeechConfig : function(lang) { 499 var langFiles = null; 500 var retValue = false; 501 502 if (lang != null) { 503 langFiles = commonUtils.getDirectoryContents(constants.getLanguagePath() + lang); 504 if (langFiles != null) { 505 if (langFiles.indexOf(constants.getSpeechConfigFileName()) > -1) { 506 retValue = true; 507 } 508 } 509 } 510 return retValue; 511 }, 512 513 /** 514 * If a JSON grammar file exists for the given language, 'true' is 515 * returned. Else the method returns 'false'. 516 * 517 * @function 518 * @returns {Boolean} True if a grammar exists for given language. 519 * @param {String} 520 * Language String, i.e.: en, de 521 * @public 522 */ 523 existsGrammar : doCheckExistsGrammar, 524 525 /** 526 * Chooses a language for the application. 527 * 528 * <p> 529 * The language selection is done as follows: 530 * <ol> 531 * <li>check if a default language exists<br> 532 * if it does and if both (!) grammar and dictionary exist for this 533 * language, return this language </li> 534 * <li>walk through all languages alphabetically 535 * <ol> 536 * <li>if for a language both (!) grammar and dictionary exist, 537 * return this language memorize the first language with a grammar 538 * (do not care, if a dictionary exists) </li> 539 * </ol> 540 * <li>test if a grammar exists for the default language - do not 541 * care about dictionaries - if it does, return the default language 542 * </li> 543 * <li>If a language was found (in Step 2.1) return this language 544 * </li> 545 * <li>If still no language is returned take the default language 546 * if it has a dictionary </li> 547 * <li>If a language exists, take it (the first one) </li> 548 * <li>Take the default language - no matter what </li> 549 * </ol> 550 * 551 * @function 552 * @returns {String} The determined language 553 * @public 554 */ 555 determineLanguage : function(lang) { 556 var tempLanguage = lang; 557 var firstLanguageWithGrammar = null; 558 559 // first check, if language - given in parameter - exists 560 if (tempLanguage != null) { 561 // check if both grammar and dictionary exist for given 562 // language 563 if (instance.existsGrammar(tempLanguage) && instance.existsDictionary(tempLanguage)) { 564 return tempLanguage; 565 } 566 } 567 568 tempLanguage = constants.getLanguage(); 569 // then check, if default language exists 570 if (tempLanguage != null) { 571 // check if both grammar and dictionary exist for default 572 // language 573 if (instance.existsGrammar(tempLanguage) && instance.existsDictionary(tempLanguage)) { 574 return tempLanguage; 575 } 576 } 577 // walk through the languages alphabetically 578 for ( var i = 0; i < languages.length; i++) { 579 tempLanguage = languages[i]; 580 // check if a grammar and dictionary exists for every 581 // language 582 if (instance.existsGrammar(tempLanguage)) { 583 584 // memorize the first language with a grammar (for 585 // later) 586 if (firstLanguageWithGrammar == null) { 587 firstLanguageWithGrammar = tempLanguage; 588 } 589 590 if (instance.existsDictionary(tempLanguage)) { 591 return tempLanguage; 592 } 593 } 594 } 595 596 // still no language found - take the default language and test 597 // if a grammar exists 598 tempLanguage = constants.getLanguage(); 599 if (tempLanguage != null) { 600 // check if both grammar and dictionary exist for default 601 // language 602 if (instance.existsGrammar(tempLanguage)) { 603 return tempLanguage; 604 } else if (firstLanguageWithGrammar != null) { 605 return firstLanguageWithGrammar; 606 } else if (instance.existsDictionary(tempLanguage)) { 607 return tempLanguage; 608 } 609 } 610 611 // still no language - take the first one 612 tempLanguage = languages[0]; 613 if (tempLanguage != null) { 614 return tempLanguage; 615 } 616 617 return constants.getLanguage(); 618 }, 619 620 /** 621 * Sets a new language, but only, if the new language is different 622 * from the current language. 623 * 624 * @function 625 * @returns {String} The (new) current language 626 * @public 627 */ 628 setLanguage : function(lang) { 629 return setLanguage(lang); 630 }, 631 632 /** 633 * Gets the language currently used for the translation. 634 * 635 * @function 636 * @returns {String} The current language 637 * @public 638 */ 639 getLanguage : function() { 640 return currentLanguage; 641 }, 642 643 /** 644 * Gets the default language. 645 * 646 * @function 647 * @returns {String} The default language 648 * @public 649 */ 650 getDefaultLanguage : function() { 651 return constants.getLanguage(); 652 }, 653 654 /** 655 * Gets an array of all for the translation available languages.<br> 656 * 657 * @function 658 * @returns {String} An array of all for the translation available 659 * languages 660 * @public 661 */ 662 getLanguages : function() { 663 return languages; 664 }, 665 666 /** 667 * Cycles through the available languages. 668 * 669 * @function 670 * @returns {String} The (new) current language 671 * @public 672 * @deprecated unused 673 */ 674 setNextLanguage : function() { 675 var indexCurrentLanguage = languages.indexOf(currentLanguage); 676 677 if (logger.isVerbose()) logger.v("[LanguageManager] Current language is " + currentLanguage); 678 679 if (indexCurrentLanguage > -1) { 680 indexCurrentLanguage = indexCurrentLanguage + 1; 681 if (indexCurrentLanguage > languages.length - 1) { 682 indexCurrentLanguage = 0; 683 } 684 currentLanguage = languages[indexCurrentLanguage]; 685 686 if (logger.isVerbose()) logger.v("[LanguageManager] Next language is " + currentLanguage); 687 688 loadSpeechConfig(currentLanguage); 689 return loadDictionary(currentLanguage); 690 } 691 return currentLanguage; 692 }, 693 694 /** 695 * Looks up a keyword in the current dictionary and returns the 696 * translation. 697 * 698 * @function 699 * @param {String} 700 * textVarName The keyword which is to be translated 701 * @returns {String} The translation of the keyword 702 * @public 703 */ 704 getText : function(textVarName) { 705 return internalGetText(textVarName); 706 }, 707 708 /** 709 * Get the language code setting for a specific plugin. 710 * 711 * Returns the default setting, if no specific setting for the specified plugin was defined. 712 * 713 * @public 714 * @param {String} pluginId 715 * @param {String|Array<String>} [feature] OPTIONAL 716 * dot-separate path String or "path Array" 717 * This parameter may be omitted, if no <code>separator</code> parameter 718 * is used. 719 * DEFAULT: "language" (the language feature) 720 * @param {String} [separator] OPTIONAL 721 * the speparator-string that should be used for separating 722 * the country-part and the language-part of the code 723 * @returns {String} the language-setting/-code 724 */ 725 getLanguageConfig : function(pluginId, feature, separator) { 726 727 //if nothing is specfied: 728 // return default language-setting 729 if(typeof pluginId === 'undefined'){ 730 return currentSpeechConfig.language; /////////// EARLY EXIT /////////////// 731 } 732 733 //ASSERT pluginId is defined 734 735 //default feature is language 736 if(typeof feature === 'undefined'){ 737 feature = 'language'; 738 } 739 740 var value; 741 if(currentSpeechConfig.plugins && currentSpeechConfig.plugins[pluginId] && typeof currentSpeechConfig.plugins[pluginId][feature] !== 'undefined'){ 742 //if there is a plugin-specific setting for this feature 743 value = currentSpeechConfig.plugins[pluginId][feature]; 744 } 745 else if(feature !== 'plugins' && typeof currentSpeechConfig[feature] !== 'undefined'){ 746 //otherwise take the default setting (NOTE: the name "plugins" is not allowed for features!) 747 value = currentSpeechConfig[feature]; 748 } 749 750 //if there is a separator specified: replace default separator '-' with this one 751 if(value && typeof separator !== 'undefined'){ 752 value = value.replace(/-/, separator); 753 } 754 755 return value; 756 } 757 758 /** 759 * Set to "backwards compatibility mode" (for pre version 2.0). 760 * 761 * This function re-adds deprecated and removed functions and 762 * properties to the CommonUtils instance. 763 * 764 * NOTE that once set to compatibility mode, it cannot be reset to 765 * non-compatibility mode. 766 * 767 * @deprecated use only for backward compatability 768 * 769 * @public 770 * @async 771 * @requires jQuery.Deferred 772 * @requires mmir.LanguageManager.setToCompatibilityModeExtension 773 * 774 * @param {Function} [success] 775 * a callback function that is invoked, after compatibility mode 776 * was set (alternatively the returned promise can be used). 777 * @returns {Promise} 778 * a Deffered.promise that is resolved, after compatibility mode 779 * was set 780 * 781 * @see mmir.LanguageManager.setToCompatibilityModeExtension 782 */ 783 , setToCompatibilityMode : function(success) { 784 785 var defer = $.Deferred(); 786 if(success){ 787 defer.always(success); 788 } 789 790 require(['languageManagerCompatibility'],function(setCompatibility){ 791 792 setCompatibility(instance); 793 794 defer.resolve(); 795 }); 796 797 return defer.promise(); 798 799 }//END: setToCompatibilityMode() 800 801 };//END: return{} 802 803 804 }//END: construcor = function(){... 805 806 807 //FIXME as of now, the LanguageManager needs to be initialized, 808 // either by calling getInstance() or init() 809 // -> should this be done explicitly (async-loading for dictionary and grammar? with returned Deferred.promise?) 810 instance = new constructor(); 811 812 return instance; 813 814 }); 815