From 00d04e0157831edb4289fc70f28fcbdedbdfbd24 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 30 Mar 2022 12:59:13 +0300 Subject: [PATCH] translatable error messages --- .../Espo/Core/Exceptions/Error/Body.php | 80 ++++++++++++ application/Espo/Core/Record/Service.php | 14 ++- .../Espo/Resources/i18n/en_US/Stream.json | 3 +- client/src/app.js | 114 +++++++++++------- 4 files changed, 168 insertions(+), 43 deletions(-) create mode 100644 application/Espo/Core/Exceptions/Error/Body.php diff --git a/application/Espo/Core/Exceptions/Error/Body.php b/application/Espo/Core/Exceptions/Error/Body.php new file mode 100644 index 0000000000..a7fe8d3cd2 --- /dev/null +++ b/application/Espo/Core/Exceptions/Error/Body.php @@ -0,0 +1,80 @@ + + */ + private ?array $messageTranslationData = null; + + public static function create(): self + { + return new self(); + } + + /** + * @param ?array $data + */ + public function withMessageTranslation(string $label, ?string $scope = null, ?array $data = null): self + { + $obj = clone $this; + + $obj->messageTranslationLabel = $label; + $obj->messageTranslationScope = $scope; + $obj->messageTranslationData = $data; + + return $obj; + } + + public function encode(): string + { + $data = (object) []; + + if ($this->messageTranslationLabel) { + $messageTranslationData = (object) ($this->messageTranslationData ?? []); + + $data->messageTranslation = (object) [ + 'label' => $this->messageTranslationLabel, + 'scope' => $this->messageTranslationScope, + 'data' => $messageTranslationData, + ]; + } + + return Json::encode($data); + } +} diff --git a/application/Espo/Core/Record/Service.php b/application/Espo/Core/Record/Service.php index 572c8631c7..faa0342a3f 100644 --- a/application/Espo/Core/Record/Service.php +++ b/application/Espo/Core/Record/Service.php @@ -32,6 +32,7 @@ namespace Espo\Core\Record; use Espo\Core\ORM\Entity as CoreEntity; use Espo\Core\Utils\Json; +use Espo\Core\Exceptions\Error\Body as ErrorBody; use Espo\Core\Exceptions\{ Error, @@ -1262,7 +1263,18 @@ class Service implements Crud, $result = $this->getStreamService()->followEntity($entity, $foreignId); if (!$result) { - throw new ForbiddenSilent("Could not add a user to followers. The user needs to have 'stream' access."); + throw ForbiddenSilent::createWithBody( + "Could not add user to followers.", + ErrorBody::create() + ->withMessageTranslation( + 'couldNotAddFollowerUserHasNoAccessToStream', + 'Stream', + [ + 'userName' => $user->getUserName(), + ] + ) + ->encode() + ); } } diff --git a/application/Espo/Resources/i18n/en_US/Stream.json b/application/Espo/Resources/i18n/en_US/Stream.json index fd939bc3fd..c1a4fb4cb6 100644 --- a/application/Espo/Resources/i18n/en_US/Stream.json +++ b/application/Espo/Resources/i18n/en_US/Stream.json @@ -1,7 +1,8 @@ { "messages": { "infoMention": "Type **@username** to mention user in the post.", - "infoSyntax": "Available markdown syntax" + "infoSyntax": "Available markdown syntax", + "couldNotAddFollowerUserHasNoAccessToStream": "Could not add the user '{userName}' to the followers. The user does not have 'stream' access to the record." }, "syntaxItems": { "code": "code", diff --git a/client/src/app.js b/client/src/app.js index 35841a4570..e22f763667 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -815,17 +815,11 @@ define( contentType: 'application/json', }); - $(document).ajaxError((event, xhr, options) => { + $(document).ajaxError((e, xhr, options) => { if (xhr.errorIsHandled) { return; } - let statusReason = xhr.getResponseHeader('X-Status-Reason'); - - let msg; - - let closeButton = false; - switch (xhr.status) { case 0: if (xhr.statusText === 'timeout') { @@ -836,6 +830,7 @@ define( case 200: Ui.error(this.language.translate('Bad server response')); + console.error('Bad server response: ' + xhr.responseText); break; @@ -844,10 +839,11 @@ define( if (!options.login) { if (this.auth) { this.logout(); + + break } - else { - console.error('Error 401: Unauthorized.'); - } + + console.error('Error 401: Unauthorized.'); } break; @@ -859,28 +855,12 @@ define( break; } - msg = this.language.translate('Error') + ' ' + xhr.status + ': ' + - this.language.translate('Access denied'); - - if (statusReason) { - msg += '\n' + statusReason; - - closeButton = true; - } - - Ui.error(msg, closeButton); + this._processErrorAlert(xhr, 'Access denied'); break; case 400: - msg = this.language.translate('Error') + ' ' + xhr.status + ': ' + - this.language.translate('Bad request'); - - if (statusReason) { - msg += ': ' + statusReason; - } - - Ui.error(msg); + this._processErrorAlert(xhr, 'Bad request'); break; @@ -891,30 +871,82 @@ define( break } - msg = this.language.translate('Error') + ' ' + xhr.status; + this._processErrorAlert(xhr, 'Not found', true); - msg += ': ' + this.language.translate('Not found'); - - Ui.error(msg); + break; default: - msg = this.language.translate('Error') + ' ' + xhr.status; - - if (statusReason) { - msg += '\n' + statusReason; - - closeButton = true; - } - - Ui.error(msg, closeButton); + this._processErrorAlert(xhr, null); } + let statusReason = xhr.getResponseHeader('X-Status-Reason'); + if (statusReason) { console.error('Server side error ' + xhr.status + ': ' + statusReason); } }); }, + _processErrorAlert: function (xhr, label, noDetail) { + let msg = this.language.translate('Error') + ' ' + xhr.status; + + if (label) { + msg += ': ' + this.language.translate(label); + } + + let obj = { + msg: msg, + closeButton: false, + }; + + let isMessageDone = false; + + if (noDetail) { + isMessageDone = true; + } + + if (!isMessageDone && xhr.responseText && xhr.responseText[0] === '{') { + let data = null; + + try { + data = JSON.parse(xhr.responseText); + } + catch (e) {} + + if (data && data.messageTranslation && data.messageTranslation.label) { + let msgDetail = this.language.translate( + data.messageTranslation.label, + 'messages', + data.messageTranslation.scope + ); + + let msgData = data.messageTranslation.data || {}; + + for (let key in msgData) { + msgDetail = msgDetail.replace('{' + key + '}', msgData[key]); + } + + obj.msg += '\n' + msgDetail; + obj.closeButton = true; + + isMessageDone = true; + } + } + + if (!isMessageDone) { + let statusReason = xhr.getResponseHeader('X-Status-Reason'); + + if (statusReason) { + obj.msg += '\n' + statusReason; + obj.closeButton = true; + + isMessageDone = true; + } + } + + Ui.error(obj.msg, obj.closeButton); + }, + initBroadcastChannel: function () { this.broadcastChannel = new BroadcastChannel();