From 3ed5b625895401a687186bce4a60e56066ae18ec Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 30 Mar 2022 23:56:42 +0000 Subject: [PATCH] GUACAMOLE-1571: Check available translations rather than hardcoding which use the default. --- .../directives/guacClientNotification.js | 112 +++++++----------- .../app/client/directives/guacFileTransfer.js | 48 +++----- .../src/app/client/services/guacTranslate.js | 82 +++++++++++++ .../client/templates/guacFileTransfer.html | 2 +- .../src/app/client/types/TranslationResult.js | 59 +++++++++ 5 files changed, 201 insertions(+), 102 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/client/services/guacTranslate.js create mode 100644 guacamole/src/main/frontend/src/app/client/types/TranslationResult.js diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js index e1a08ebc6..03344b627 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js @@ -34,16 +34,16 @@ angular.module('client').directive('guacClientNotification', [function guacClien /** * The client whose status should be displayed. - * + * * @type ManagedClient */ client : '=' - + }; directive.controller = ['$scope', '$injector', '$element', function guacClientNotificationController($scope, $injector, $element) { - + // Required types const ManagedClient = $injector.get('ManagedClient'); const ManagedClientState = $injector.get('ManagedClientState'); @@ -53,6 +53,7 @@ angular.module('client').directive('guacClientNotification', [function guacClien const $location = $injector.get('$location'); const authenticationService = $injector.get('authenticationService'); const guacClientManager = $injector.get('guacClientManager'); + const guacTranslate = $injector.get('guacTranslate'); const requestService = $injector.get('requestService'); const userPageService = $injector.get('userPageService'); @@ -65,26 +66,6 @@ angular.module('client').directive('guacClientNotification', [function guacClien */ $scope.status = false; - /** - * All client error codes handled and passed off for translation. Any error - * code not present in this list will be represented by the "DEFAULT" - * translation. - */ - const CLIENT_ERRORS = { - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0207: true, - 0x0208: true, - 0x0209: true, - 0x020A: true, - 0x020B: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - /** * All error codes for which automatic reconnection is appropriate when a * client error occurs. @@ -98,26 +79,7 @@ angular.module('client').directive('guacClientNotification', [function guacClien 0x0301: true, 0x0308: true }; - - /** - * All tunnel error codes handled and passed off for translation. Any error - * code not present in this list will be represented by the "DEFAULT" - * translation. - */ - const TUNNEL_ERRORS = { - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0204: true, - 0x0205: true, - 0x0207: true, - 0x0208: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - + /** * All error codes for which automatic reconnection is appropriate when a * tunnel error occurs. @@ -239,7 +201,7 @@ angular.module('client').directive('guacClientNotification', [function guacClien // Get any associated status code const status = $scope.client.clientState.statusCode; - // Connecting + // Connecting if (connectionState === ManagedClientState.ConnectionState.CONNECTING || connectionState === ManagedClientState.ConnectionState.WAITING) { $scope.status = { @@ -254,44 +216,58 @@ angular.module('client').directive('guacClientNotification', [function guacClien // Client error else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) { - // Determine translation name of error - const errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + // Translation IDs for this error code + const errorPrefix = "CLIENT.ERROR_CLIENT_"; + const errorId = errorPrefix + status.toString(16).toUpperCase(); + const defaultErrorId = errorPrefix + "DEFAULT"; // Determine whether the reconnect countdown applies const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - // Show error status - notifyConnectionClosed({ - className : "error", - title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : { - key : "CLIENT.ERROR_CLIENT_" + errorName - }, - countdown : countdown, - actions : actions - }); + // Use the guacTranslate service to determine if there is a translation for + // this error code; if not, use the default + guacTranslate(errorId, defaultErrorId).then( + + // Show error status + translationResult => notifyConnectionClosed({ + className : "error", + title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text : { + key : translationResult.id + }, + countdown : countdown, + actions : actions + }) + ); } // Tunnel error else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { - // Determine translation name of error - const errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + // Translation IDs for this error code + const errorPrefix = "CLIENT.ERROR_TUNNEL_"; + const errorId = errorPrefix + status.toString(16).toUpperCase(); + const defaultErrorId = errorPrefix + "DEFAULT"; // Determine whether the reconnect countdown applies const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - // Show error status - notifyConnectionClosed({ - className : "error", - title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : { - key : "CLIENT.ERROR_TUNNEL_" + errorName - }, - countdown : countdown, - actions : actions - }); + // Use the guacTranslate service to determine if there is a translation for + // this error code; if not, use the default + guacTranslate(errorId, defaultErrorId).then( + + // Show error status + translationResult => notifyConnectionClosed({ + className : "error", + title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text : { + key : translationResult.id + }, + countdown : countdown, + actions : actions + }) + ); } diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js b/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js index a9c09bca5..d016a721c 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js @@ -30,7 +30,7 @@ angular.module('client').directive('guacFileTransfer', [function guacFileTransfe /** * The file transfer to display. - * + * * @type ManagedFileUpload|ManagedFileDownload */ transfer : '=' @@ -40,27 +40,12 @@ angular.module('client').directive('guacFileTransfer', [function guacFileTransfe templateUrl: 'app/client/templates/guacFileTransfer.html', controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) { + // Required services + const guacTranslate = $injector.get('guacTranslate'); + // Required types var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); - /** - * All upload error codes handled and passed off for translation. - * Any error code not present in this list will be represented by - * the "DEFAULT" translation. - */ - var UPLOAD_ERRORS = { - 0x0100: true, - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0204: true, - 0x0205: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - /** * Returns the unit string that is most appropriate for the * number of bytes transferred thus far - either 'gb', 'mb', 'kb', @@ -193,7 +178,7 @@ angular.module('client').directive('guacFileTransfer', [function guacFileTransfe return; // Save file - saveAs($scope.transfer.blob, $scope.transfer.filename); + saveAs($scope.transfer.blob, $scope.transfer.filename); }; @@ -210,23 +195,20 @@ angular.module('client').directive('guacFileTransfer', [function guacFileTransfe return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR; }; - /** - * Returns the text of the current error as a translation string. - * - * @returns {String} - * The name of the translation string containing the text - * associated with the current error. - */ - $scope.getErrorText = function getErrorText() { + // The translated error message for the current status code + $scope.translatedErrorMessage = ''; + + $scope.$watch('transfer.transferState.statusCode', function statusCodeChanged(statusCode) { // Determine translation name of error - var status = $scope.transfer.transferState.statusCode; - var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + const errorName = 'CLIENT.ERROR_UPLOAD_' + statusCode.toString(16).toUpperCase(); - // Return translation string - return 'CLIENT.ERROR_UPLOAD_' + errorName; + // Use translation string, or the default if no translation is found for this error code + guacTranslate(errorName, 'CLIENT.ERROR_UPLOAD_DEFAULT').then( + translationResult => $scope.translatedErrorMessage = translationResult.message + ); - }; + }); }] // end file transfer controller diff --git a/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js new file mode 100644 index 000000000..f280e81d7 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A wrapper around the angular-translate $translate service that offers a + * convenient way to fall back to a default translation if the requested + * translation is not available. + */ + angular.module('client').factory('guacTranslate', ['$injector', function guacTranslate($injector) { + + // Required services + const $q = $injector.get('$q'); + const $translate = $injector.get('$translate'); + + // Required types + const TranslationResult = $injector.get('TranslationResult'); + + /** + * Returns a promise that will be resolved with a TranslationResult containg either the + * requested ID and message (if translated), or the default ID and message if translated, + * or the literal value of `defaultTranslationId` for both the ID and message if neither + * is translated. + * + * @param {String} translationId + * The requested translation ID, which may or may not be translated. + * + * @param {Sting} defaultTranslationId + * The translation ID that will be used if no translation is found for `translationId`. + * + * @returns {Promise.} + * A promise which resolves with a TranslationResult containing the results from + * the translation attempt. + */ + function translateWithFallback(translationId, defaultTranslationId) { + const deferredTranslation = $q.defer(); + + // Attempt to translate the requested translation ID + $translate(translationId).then( + + // If the requested translation is available, use that + translation => deferredTranslation.resolve(new TranslationResult({ + id: translationId, message: translation + })), + + // Otherwise, try the default translation ID + () => $translate(defaultTranslationId).then( + + // Default translation worked, so use that + defaultTranslation => + deferredTranslation.resolve(new TranslationResult({ + id: defaultTranslationId, message: defaultTranslation + })), + + // Neither translation is available; as a fallback, return default ID for both + () => deferredTranslation.resolve(new TranslationResult({ + id: defaultTranslationId, message: defaultTranslationId + })), + ) + ); + + return deferredTranslation.promise; + }; + + return translateWithFallback; + +}]); diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html b/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html index dd96baaf4..32ead84e2 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html +++ b/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html @@ -10,7 +10,7 @@ -

{{getErrorText() | translate}}

+

{{translatedErrorMessage}}

diff --git a/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js b/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js new file mode 100644 index 000000000..0a81511bb --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Provides the TranslationResult class used by the guacTranslate service. This class contains + * both the translated message and the translation ID that generated the message, in the case + * where it's unknown whether a translation is defined or not. + */ + angular.module('client').factory('TranslationResult', [function defineTranslationResult() { + + /** + * Object which represents the result of a translation as returned from + * the guacTranslate service. + * + * @constructor + * @param {TranslationResult|Object} [template={}] + * The object whose properties should be copied within the new + * TranslationResult. + */ + const TranslationResult = function TranslationResult(template) { + + // Use empty object by default + template = template || {}; + + /** + * The translation ID. + * + * @type {String} + */ + this.id = template.id; + + /** + * The translated message. + * + * @type {String} + */ + this.message = template.message; + + }; + + return TranslationResult; + +}]); \ No newline at end of file