From 1610073834daa6331ea3664f18a4a82f9adc4aab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tam=C3=A1s=20Moln=C3=A1r?=
Date: Sun, 14 May 2017 21:03:18 +0200
Subject: [PATCH] Add third party translation (WIP)
---
Module.php | 8 ++
assets/javascripts/translate.js | 16 +++
controllers/LanguageController.php | 7 +-
controllers/actions/TranslateAction.php | 1 +
controllers/actions/TranslateTextAction.php | 77 ++++++++++++++
translation/BaseTranslator.php | 28 +++++
translation/Exception.php | 12 +++
translation/GoogleTranslator.php | 111 ++++++++++++++++++++
translation/Translator.php | 48 +++++++++
translation/client/ClientException.php | 14 +++
translation/client/CurlApiClient.php | 71 +++++++++++++
translation/client/TranslatorApiClient.php | 26 +++++
views/language/translate.php | 14 ++-
13 files changed, 429 insertions(+), 4 deletions(-)
create mode 100644 controllers/actions/TranslateTextAction.php
create mode 100644 translation/BaseTranslator.php
create mode 100644 translation/Exception.php
create mode 100644 translation/GoogleTranslator.php
create mode 100644 translation/Translator.php
create mode 100644 translation/client/ClientException.php
create mode 100644 translation/client/CurlApiClient.php
create mode 100644 translation/client/TranslatorApiClient.php
diff --git a/Module.php b/Module.php
index 01dd20e..63ce25d 100644
--- a/Module.php
+++ b/Module.php
@@ -286,6 +286,14 @@ class Module extends \yii\base\Module
*/
public $scanners = [];
+ /**
+ * @var array Configuration for a third-party translator (e.g.: Google, Yandex, etc.) that can be used for
+ * text translation.
+ *
+ * The translator should implement the `lajax\translatemanager\translation\Translator` interface.
+ */
+ public $translator;
+
/**
* @inheritdoc
*/
diff --git a/assets/javascripts/translate.js b/assets/javascripts/translate.js
index 47c6f07..ea8a5a9 100644
--- a/assets/javascripts/translate.js
+++ b/assets/javascripts/translate.js
@@ -42,11 +42,27 @@ var translate = (function () {
_translateLanguage($this);
}
+ /**
+ * @param {$} $btn
+ */
+ function _translateText($btn) {
+ var $translation = $btn.closest('tr').find('.translation');
+ console.log($translation);
+
+ helpers.post('/translatemanager/language/translate-text', {
+ id: $translation.data('id'),
+ language_id: $('#language_id').val()
+ });
+ }
+
return {
init: function () {
$('#translates').on('click', '.source', function () {
_copySourceToTranslation($(this));
});
+ $('#translates').on('click', '.js-translate-text', function () {
+ _translateText($(this));
+ });
$('#translates').on('click', 'button', function () {
_translateLanguage($(this));
});
diff --git a/controllers/LanguageController.php b/controllers/LanguageController.php
index 363cc58..66ae1d6 100644
--- a/controllers/LanguageController.php
+++ b/controllers/LanguageController.php
@@ -35,11 +35,11 @@ public function behaviors()
return [
'access' => [
'class' => AccessControl::className(),
- 'only' => ['list', 'change-status', 'optimizer', 'scan', 'translate', 'save', 'dialog', 'message', 'view', 'create', 'update', 'delete', 'delete-source', 'import', 'export'],
+ 'only' => ['list', 'change-status', 'optimizer', 'scan', 'translate', 'translate-text', 'save', 'dialog', 'message', 'view', 'create', 'update', 'delete', 'delete-source', 'import', 'export'],
'rules' => [
[
'allow' => true,
- 'actions' => ['list', 'change-status', 'optimizer', 'scan', 'translate', 'save', 'dialog', 'message', 'view', 'create', 'update', 'delete', 'delete-source', 'import', 'export'],
+ 'actions' => ['list', 'change-status', 'optimizer', 'scan', 'translate', 'translate-text', 'save', 'dialog', 'message', 'view', 'create', 'update', 'delete', 'delete-source', 'import', 'export'],
'roles' => $this->module->roles,
],
],
@@ -71,6 +71,9 @@ public function actions()
'translate' => [
'class' => 'lajax\translatemanager\controllers\actions\TranslateAction',
],
+ 'translate-text' => [
+ 'class' => 'lajax\translatemanager\controllers\actions\TranslateTextAction',
+ ],
'save' => [
'class' => 'lajax\translatemanager\controllers\actions\SaveAction',
],
diff --git a/controllers/actions/TranslateAction.php b/controllers/actions/TranslateAction.php
index d22dd4b..c80edc6 100644
--- a/controllers/actions/TranslateAction.php
+++ b/controllers/actions/TranslateAction.php
@@ -42,6 +42,7 @@ public function run()
'dataProvider' => $dataProvider,
'searchModel' => $searchModel,
'searchEmptyCommand' => $this->controller->module->searchEmptyCommand,
+ 'isTranslationApiAvailable' => !empty($this->controller->module->translator),
'language_id' => Yii::$app->request->get('language_id', ''),
]);
}
diff --git a/controllers/actions/TranslateTextAction.php b/controllers/actions/TranslateTextAction.php
new file mode 100644
index 0000000..8ef91f3
--- /dev/null
+++ b/controllers/actions/TranslateTextAction.php
@@ -0,0 +1,77 @@
+response->format = Response::FORMAT_JSON;
+
+ $id = Yii::$app->request->post('id', 0);
+ $languageId = Yii::$app->request->post('language_id', Yii::$app->language);
+
+ $languageTranslate = LanguageTranslate::findOne(['id' => $id, 'language' => $languageId]) ?:
+ new LanguageTranslate(['id' => $id, 'language' => $languageId]);
+
+ try {
+ $languageTranslate->translation = $this->translateText($id, $languageId);
+ } catch (BaseException $e) {
+ Yii::error('Translation failed! ' . $e->getMessage(), 'translatemanager');
+ $languageTranslate->addError('translation', 'API translation failed!');
+ }
+
+ if ($languageTranslate->validate(null, false) && $languageTranslate->save()) {
+ $generator = new Generator($this->controller->module, $languageId);
+ $generator->run();
+ }
+
+ return $languageTranslate->getErrors();
+ }
+
+ /**
+ * @param int $sourceId
+ * @param string $languageId
+ *
+ * @return string
+ *
+ * @throws BaseException
+ */
+ protected function translateText($sourceId, $languageId)
+ {
+ if (!$this->controller->module->translator) {
+ throw new BaseException('No translator configured!');
+ }
+
+ $source = LanguageSource::findOne($sourceId);
+ if (!$source) {
+ throw new BaseException('Invalid language source id!');
+ }
+
+ /* @var $translator Translator */
+ $translator = Yii::createObject($this->controller->module->translator);
+
+ try {
+ return $translator->translate($source->message, $languageId);
+ } catch (TranslationException $e) {
+ throw new BaseException('Translation failed: ' . $e->getMessage(), 1, $e);
+ }
+ }
+}
diff --git a/translation/BaseTranslator.php b/translation/BaseTranslator.php
new file mode 100644
index 0000000..a7ce175
--- /dev/null
+++ b/translation/BaseTranslator.php
@@ -0,0 +1,28 @@
+ 'lajax\translatemanager\translation\client\CurlApiClient',
+ ];
+
+ public function init()
+ {
+ parent::init();
+
+ $this->apiClient = Yii::createObject($this->apiClient);
+ }
+}
diff --git a/translation/Exception.php b/translation/Exception.php
new file mode 100644
index 0000000..8e438d4
--- /dev/null
+++ b/translation/Exception.php
@@ -0,0 +1,12 @@
+ 'lajax\translatemanager\translation\GoogleTranslator',
+ * 'apiKey' => 'YOUR_API_KEY',
+ * ]
+ * ```
+ *
+ * @author moltam
+ */
+class GoogleTranslator extends BaseTranslator
+{
+ /**
+ * @var string The API key for authentication.
+ */
+ public $apiKey;
+
+ /**
+ * @var string
+ */
+ private static $baseApiUrl = 'https://translation.googleapis.com';
+
+ /**
+ * @inheritdoc
+ */
+ public function translate($text, $target, $source = null, $format = 'html')
+ {
+ $response = $this->apiClient->send($this->buildApiUrl('/language/translate/v2'), 'POST', [
+ 'key' => $this->apiKey,
+ 'q' => $text,
+ 'target' => substr($target, 0, 2),
+ 'source' => substr($source, 0, 2),
+ 'format' => $format,
+ ]);
+
+ $decodesResponse = $this->decodeResponse($response);
+ $this->checkResponse($decodesResponse);
+
+ return $decodesResponse['data']['translations'][0]['translatedText'];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function detect($text)
+ {
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getLanguages()
+ {
+ }
+
+ /**
+ * @param string $uri
+ * @param array $queryParams [optional]
+ *
+ * @return string
+ */
+ private function buildApiUrl($uri, array $queryParams = [])
+ {
+ return self::$baseApiUrl . $uri . '?' . http_build_query($queryParams);
+ }
+
+ /**
+ * @param string $response
+ *
+ * @return array
+ *
+ * @throws Exception
+ */
+ private function decodeResponse($response)
+ {
+ $decodesResponse = Json::decode($response);
+ if (!$decodesResponse) {
+ throw new Exception('Invalid API response: json decode failed!');
+ }
+
+ return $decodesResponse;
+ }
+
+ /**
+ * @param array $response
+ *
+ * @throws Exception
+ */
+ private function checkResponse($response)
+ {
+ if (isset($response['error'])) {
+ $error = $response['error'];
+ Yii::error('API response: ' . var_export($response, true), 'translatemanager');
+
+ throw new Exception("API error: $error[message]", $error['code']);
+ }
+ }
+}
diff --git a/translation/Translator.php b/translation/Translator.php
new file mode 100644
index 0000000..72dd7fc
--- /dev/null
+++ b/translation/Translator.php
@@ -0,0 +1,48 @@
+The language code of the source text. If not given, the translator tries to detect the language.
+ * @param string $format [optional]
+ * The format of the source text. Possible values:
+ * - html: string with HTML markup,
+ * - text: plain text without markup.
+ *
+ *
+ * @return string The translation.
+ *
+ * @throws Exception If the text cannot be translated, or an error occurred during translation.
+ */
+ public function translate($text, $target, $source = null, $format = 'html');
+
+ /**
+ * Detects the language of the specified text.
+ *
+ * @param string $text The analyzed text.
+ *
+ * @return string The language code for translation.
+ *
+ * @throws Exception If the language cannot be detected, or an error occurred during detection.
+ */
+ public function detect($text);
+
+ /**
+ * Returns the languages supported by the translator.
+ *
+ * @return string[] A list of language codes.
+ */
+ public function getLanguages();
+}
diff --git a/translation/client/ClientException.php b/translation/client/ClientException.php
new file mode 100644
index 0000000..858b429
--- /dev/null
+++ b/translation/client/ClientException.php
@@ -0,0 +1,14 @@
+ true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_MAXREDIRS => 5,
+ ];
+
+ /**
+ * @inheritdoc
+ */
+ public function send($url, $httpMethod = 'GET', array $bodyParams = [])
+ {
+ Yii::trace("Sending $httpMethod request to: $url", 'translatemanager');
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt_array($ch, $this->curlOptions);
+ curl_setopt_array($ch, $this->getHttpMethodOptions($httpMethod, $bodyParams));
+
+ $response = curl_exec($ch);
+ $errno = curl_errno($ch);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ if ($errno) {
+ throw new ClientException("cURL error occured: $error", $errno);
+ }
+
+ return $response;
+ }
+
+ /**
+ * @param string $httpMethod
+ * @param array $bodyParams [optional]
+ *
+ * @return array
+ *
+ * @throws NotSupportedException
+ */
+ protected function getHttpMethodOptions($httpMethod, array $bodyParams = [])
+ {
+ switch ($httpMethod) {
+ case 'GET':
+ return []; // GET is default, no options needed.
+ case 'POST':
+ return [
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => http_build_query($bodyParams),
+ ];
+ default:
+ throw new NotSupportedException("Unsupported HTTP method: $httpMethod.");
+ }
+ }
+}
diff --git a/translation/client/TranslatorApiClient.php b/translation/client/TranslatorApiClient.php
new file mode 100644
index 0000000..127d1f9
--- /dev/null
+++ b/translation/client/TranslatorApiClient.php
@@ -0,0 +1,26 @@
+The used HTTP method (GET, POSt, etc).
+ * @param array $bodyParams [optional]
+ * The parameters sent in the request body (e.g. for an POST request). Query parameters cannot be sent here.
+ *
+ * @return string The raw response returned from the API.
+ *
+ * @throws ClientException If an error occurs during the communication (e.g.: HTTP 4xx, 5xx code, etc.).
+ */
+ public function send($url, $httpMethod = 'GET', array $bodyParams = []);
+}
diff --git a/views/language/translate.php b/views/language/translate.php
index 54de2ff..b6ec62f 100644
--- a/views/language/translate.php
+++ b/views/language/translate.php
@@ -17,6 +17,7 @@
/* @var $dataProvider yii\data\ActiveDataProvider */
/* @var $searchModel lajax\translatemanager\models\searches\LanguageSourceSearch */
/* @var $searchEmptyCommand string */
+/* @var $isTranslationApiAvailable bool */
$this->title = Yii::t('language', 'Translation into {language_id}', ['language_id' => $language_id]);
$this->params['breadcrumbs'][] = ['label' => Yii::t('language', 'Languages'), 'url' => ['list']];
@@ -53,8 +54,17 @@
'attribute' => 'message',
'filterInputOptions' => ['class' => 'form-control', 'id' => 'message'],
'label' => Yii::t('language', 'Source'),
- 'content' => function ($data) {
- return Html::textarea('LanguageSource[' . $data->id . ']', $data->source, ['class' => 'form-control source', 'readonly' => 'readonly']);
+ 'content' => function ($data) use ($isTranslationApiAvailable) {
+ $content = Html::textarea('LanguageSource[' . $data->id . ']', $data->source, ['class' => 'form-control source', 'readonly' => 'readonly']);
+
+ if ($isTranslationApiAvailable) {
+ $content .= Html::button(
+ ' Translate',
+ ['class' => 'btn btn-default pull-right js-translate-text']
+ );
+ }
+
+ return $content;
},
],
[