diff --git a/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js b/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js index ecc6281a3..c73aa7770 100644 --- a/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,21 +23,25 @@ /** * The configuration block for setting up everything having to do with i18n. */ -angular.module('index').config(['$translateProvider', function($translateProvider) { +angular.module('index').config(['$injector', function($injector) { - // TODONT: Use US English by default (this should come from preferences) - $translateProvider.preferredLanguage('en_US'); + // Required providers + var $translateProvider = $injector.get('$translateProvider'); + var preferenceServiceProvider = $injector.get('preferenceServiceProvider'); - // Use US English for any undefined strings - $translateProvider.fallbackLanguage('en_US'); + // Fallback to US English + var fallbackLanguages = ['en_US']; - // Load translations from static JSON files - $translateProvider.useStaticFilesLoader({ - prefix: 'translations/', - suffix: '.json' + // Prefer chosen language, use fallback languages if necessary + $translateProvider.fallbackLanguage(fallbackLanguages); + $translateProvider.preferredLanguage(preferenceServiceProvider.preferences.language); + + // Load translations via translationLoader service + $translateProvider.useLoader('translationLoader', { + fallbackLanguages : fallbackLanguages }); // Provide pluralization, etc. via messageformat.js $translateProvider.useMessageFormatInterpolation(); -}]); \ No newline at end of file +}]); diff --git a/guacamole/src/main/webapp/app/locale/services/translationLoader.js b/guacamole/src/main/webapp/app/locale/services/translationLoader.js new file mode 100644 index 000000000..b80699e42 --- /dev/null +++ b/guacamole/src/main/webapp/app/locale/services/translationLoader.js @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * 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. + */ + +/** + * Service for loading translation definition files, conforming to the + * angular-translate documentation for custom translation loaders: + * + * https://github.com/angular-translate/angular-translate/wiki/Asynchronous-loading#using-custom-loader-service + */ +angular.module('locale').factory('translationLoader', ['$injector', function translationLoader($injector) { + + // Required services + var $http = $injector.get('$http'); + var $q = $injector.get('$q'); + var cacheService = $injector.get('cacheService'); + + /** + * Satisfies a translation request for the given key by searching for the + * translation files for each key in the given array, in order. The request + * fails only if none of the files can be found. + * + * @param {Deferred} deferred + * The Deferred object to resolve or reject depending on whether at + * least one translation file can be successfully loaded. + * + * @param {String} requestedKey + * The originally-requested language key. + * + * @param {String[]} remainingKeys + * The keys of the languages to attempt to load, in order, where the + * first key in this array is the language to try within this function + * call. The first key in the array is not necessarily the originally- + * requested language key. + */ + var satisfyTranslation = function satisfyTranslation(deferred, requestedKey, remainingKeys) { + + // Get current language key + var currentKey = remainingKeys.shift(); + + // If no languages to try, just fail + if (!currentKey) { + deferred.reject(requestedKey); + return; + } + + // Attempt to retrieve language + $http({ + cache : cacheService.languages, + method : 'GET', + url : 'translations/' + encodeURIComponent(currentKey) + '.json' + }) + + // Resolve promise if translation retrieved successfully + .success(function translationFileRetrieved(translation) { + deferred.resolve(translation); + }) + + // Retry with remaining languages if translation file could not be retrieved + .error(function translationFileUnretrievable() { + satisfyTranslation(deferred, requestedKey, remainingKeys); + }); + + }; + + /** + * Custom loader function for angular-translate which loads the desired + * language file dynamically via HTTP. If the language file cannot be + * found, the fallback language is used instead. + * + * @param {Object} options + * Arbitrary options, containing at least a "key" property which + * contains the requested language key. + * + * @returns {Promise.} + * A promise which resolves to the requested translation string object. + */ + return function loadTranslationFile(options) { + + var translation = $q.defer(); + + // Get requested language from options + var requestedKey = options.key; + + // Append fallback languages to requested language + var keys = [requestedKey].concat(options.fallbackLanguages); + + // Satisfy the translation request + satisfyTranslation(translation, requestedKey, keys); + + // Return promise which is resolved only after the translation file is loaded + return translation.promise; + + }; + +}]); diff --git a/guacamole/src/main/webapp/app/settings/services/preferenceService.js b/guacamole/src/main/webapp/app/settings/services/preferenceService.js index f0712631b..3c20aa179 100644 --- a/guacamole/src/main/webapp/app/settings/services/preferenceService.js +++ b/guacamole/src/main/webapp/app/settings/services/preferenceService.js @@ -24,16 +24,20 @@ * A service for setting and retrieving browser-local preferences. Preferences * may be any JSON-serializable type. */ -angular.module('settings').factory('preferenceService', ['$injector', - function preferenceService($injector) { +angular.module('settings').provider('preferenceService', function preferenceServiceProvider() { - // Required services - var $rootScope = $injector.get('$rootScope'); - var $window = $injector.get('$window'); + /** + * Reference to the provider itself. + * + * @type preferenceServiceProvider + */ + var provider = this; - var service = {}; - - // The parameter name for getting the history from local storage + /** + * The storage key of Guacamole preferences within local storage. + * + * @type String + */ var GUAC_PREFERENCES_STORAGE_KEY = "GUAC_PREFERENCES"; /** @@ -41,7 +45,7 @@ angular.module('settings').factory('preferenceService', ['$injector', * * @type Object. */ - service.inputMethods = { + var inputMethods = { /** * No input method is used. Keyboard events are generated from a @@ -97,7 +101,7 @@ angular.module('settings').factory('preferenceService', ['$injector', * * @type Object. */ - service.preferences = { + this.preferences = { /** * Whether translation of touch to mouse events should emulate an @@ -113,7 +117,7 @@ angular.module('settings').factory('preferenceService', ['$injector', * * @type String */ - inputMethod : service.inputMethods.NONE, + inputMethod : inputMethods.NONE, /** * The key of the desired display language. @@ -124,45 +128,71 @@ angular.module('settings').factory('preferenceService', ['$injector', }; - /** - * Persists the current values of all preferences, if possible. - */ - service.save = function save() { - - // Save updated preferences, ignore inability to use localStorage - try { - if (localStorage) - localStorage.setItem(GUAC_PREFERENCES_STORAGE_KEY, JSON.stringify(service.preferences)); - } - catch (ignore) {} - - }; - // Get stored preferences, ignore inability to use localStorage try { if (localStorage) { var preferencesJSON = localStorage.getItem(GUAC_PREFERENCES_STORAGE_KEY); if (preferencesJSON) - angular.extend(service.preferences, JSON.parse(preferencesJSON)); + angular.extend(provider.preferences, JSON.parse(preferencesJSON)); } } catch (ignore) {} - // Persist settings when window is unloaded - $window.addEventListener('unload', service.save); + // Factory method required by provider + this.$get = ['$injector', function preferenceServiceFactory($injector) { - // Persist settings upon navigation - $rootScope.$on('$routeChangeSuccess', function handleNavigate() { - service.save(); - }); + // Required services + var $rootScope = $injector.get('$rootScope'); + var $window = $injector.get('$window'); - // Persist settings upon logout - $rootScope.$on('guacLogout', function handleLogout() { - service.save(); - }); + var service = {}; - return service; + /** + * All valid input method type names. + * + * @type Object. + */ + service.inputMethods = inputMethods; -}]); + /** + * All currently-set preferences, as name/value pairs. Each property name + * corresponds to the name of a preference. + * + * @type Object. + */ + service.preferences = provider.preferences; + + /** + * Persists the current values of all preferences, if possible. + */ + service.save = function save() { + + // Save updated preferences, ignore inability to use localStorage + try { + if (localStorage) + localStorage.setItem(GUAC_PREFERENCES_STORAGE_KEY, JSON.stringify(service.preferences)); + } + catch (ignore) {} + + }; + + // Persist settings when window is unloaded + $window.addEventListener('unload', service.save); + + // Persist settings upon navigation + $rootScope.$on('$routeChangeSuccess', function handleNavigate() { + service.save(); + }); + + // Persist settings upon logout + $rootScope.$on('guacLogout', function handleLogout() { + service.save(); + }); + + return service; + + }]; + +});