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; }, ], [