From 56e7cc1f06a3098b576c6c12e3b1b85df9a52ca5 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Tue, 28 Sep 2021 22:14:59 +0200 Subject: [PATCH 1/4] FEATURE: Translate CR Nodes on create. This has still lots of limitations an probably needs tweaking. --- .../Domain/Service/NodeTranslationService.php | 66 +++++++++++++++++++ Classes/Package.php | 23 +++++++ Configuration/Settings.yaml | 1 + composer.json | 3 +- 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 Classes/Domain/Service/NodeTranslationService.php create mode 100644 Classes/Package.php diff --git a/Classes/Domain/Service/NodeTranslationService.php b/Classes/Domain/Service/NodeTranslationService.php new file mode 100644 index 0000000..9447a63 --- /dev/null +++ b/Classes/Domain/Service/NodeTranslationService.php @@ -0,0 +1,66 @@ +getNodeType()->getProperties(); + $adoptedNode = $context->getNodeByIdentifier($node->getIdentifier()); + + $sourceLanguage = explode('_', $node->getContext()->getTargetDimensions()['language'])[0]; + $targetLanguage = explode('_', $context->getTargetDimensions()['language'])[0]; + + foreach ($node->getProperties() as $propertyName => $propertyValue) { + + if (empty($propertyValue)) { + continue; + } + if (!array_key_exists($propertyName, $propertyDefinitions)) { + continue; + } + if ($propertyDefinitions[$propertyName]['type'] != 'string' || !is_string($propertyValue)) { + continue; + } + + $translateProperty = false; + $isInlineEditable = $propertyDefinitions[$propertyName]['ui']['inlineEditable'] ?? false; + $isTranslateEnabled = $propertyDefinitions[$propertyName]['options']['autotranslate'] ?? false; + if ($this->translateRichtextProperties && $isInlineEditable == true) { + $translateProperty = true; + } + if ($isTranslateEnabled) { + $translateProperty = true; + } + + if ($translateProperty) { + $translatedValue = $this->deeplService->translate($propertyValue, $targetLanguage, $sourceLanguage); + $adoptedNode->setProperty($propertyName, $translatedValue); + } + } + } +} diff --git a/Classes/Package.php b/Classes/Package.php new file mode 100644 index 0000000..882869b --- /dev/null +++ b/Classes/Package.php @@ -0,0 +1,23 @@ +getSignalSlotDispatcher(); + $dispatcher->connect(Context::class, 'afterAdoptNode', NodeTranslationService::class, 'afterAdoptNode'); + } +} diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 1b0de68..3f1ffea 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -3,6 +3,7 @@ CodeQ: DeepLService: baseUri: 'https://api.deepl.com/v2/' apiAuthKey: '' + translateRichtextProperties: true Neos: Fusion: diff --git a/composer.json b/composer.json index 250f9cf..cdeb45d 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "type": "neos-package", "name": "codeq/deepltranslationhelper", "require": { - "neos/flow": "*" + "neos/flow": "*", + "guzzlehttp/guzzle": "^7.0" }, "autoload": { "psr-4": { From fe0a17b2bb5970f0576dcbe2d36d05dc2773ba84 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 29 Sep 2021 18:45:18 +0200 Subject: [PATCH 2/4] TASK: Move translation cache to the EelHelper This is in preparation of translating multiple texts with one api call in the deepl service. --- Classes/Domain/Service/DeepLService.php | 98 +++++++------------ .../Domain/Service/NodeTranslationService.php | 2 +- Classes/EelHelper/TranslationHelper.php | 16 ++- Configuration/Objects.yaml | 2 +- 4 files changed, 55 insertions(+), 63 deletions(-) diff --git a/Classes/Domain/Service/DeepLService.php b/Classes/Domain/Service/DeepLService.php index d38448c..3953817 100644 --- a/Classes/Domain/Service/DeepLService.php +++ b/Classes/Domain/Service/DeepLService.php @@ -2,12 +2,10 @@ namespace CodeQ\DeepLTranslationHelper\Domain\Service; +use Neos\Flow\Annotations as Flow; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\GuzzleException; -use Neos\Cache\Frontend\VariableFrontend; -use Neos\Flow\Annotations as Flow; -use Neos\Flow\Aop\Exception\InvalidArgumentException; use Psr\Log\LoggerInterface; /** @@ -26,11 +24,6 @@ class DeepLService */ protected array $settings; - /** - * @var VariableFrontend - */ - protected $translationCache; - /** * @Flow\Inject * @var LoggerInterface @@ -65,65 +58,50 @@ public function translate( return $text; } - // See: https://ideone.com/embed/0iwuGn - $cacheIdentifier = sprintf('%s-%s', hash('haval256,3', $text), - $targetLanguage); - $translatedText = $this->translationCache->get($cacheIdentifier); + try { + $response = $this->deeplClient->get('translate', [ + 'query' => [ + 'text' => $text, + 'source_lang' => $sourceLanguage, + 'target_lang' => $targetLanguage, + 'tag_handling' => 'xml', + 'split_sentences' => 'nonewlines' + ] + ]); - if ($translatedText === false) { - try { - $response = $this->deeplClient->get('translate', [ - 'query' => [ - 'text' => $text, - 'source_lang' => $sourceLanguage, - 'target_lang' => $targetLanguage, - 'tag_handling' => 'xml', - 'split_sentences' => 'nonewlines' - ] + $responseBody = json_decode($response->getBody()->getContents(), + true); + $translations = $responseBody['translations']; + $translatedText = $translations[0]['text']; + } catch (ClientException $e) { + if ($e->getResponse()->getStatusCode() === 403) { + $this->logger->critical('Your DeepL API credentials are either wrong, or you don\'t have access to the requested API.'); + } elseif ($e->getResponse()->getStatusCode() === 429) { + $this->logger->warning('You sent too many requests to the DeepL API, we\'ll retry to connect to the API on the next request'); + } elseif ($e->getResponse()->getStatusCode() === 456) { + $this->logger->warning('You reached your DeepL API character limit. Upgrade your plan or wait until your quota is filled up again.'); + } elseif ($e->getResponse()->getStatusCode() === 400) { + $this->logger->warning('Your DeepL API request was not well-formed. Please check the source and the target language in particular.', [ + 'sourceLanguage' => $sourceLanguage, + 'targetLanguage' => $targetLanguage ]); - - $responseBody = json_decode($response->getBody()->getContents(), - true); - $translations = $responseBody['translations']; - $translatedText = $translations[0]['text']; - try { - $this->translationCache->set($cacheIdentifier, - $translatedText); - } catch (\Neos\Cache\Exception $e) { - $this->logger->critical('Wrong cache frontend configuration for CodeQ_DeepLTranslationHelper_Translation cache defined!'); - } catch (InvalidArgumentException $e) { - $this->logger->critical($e->getMessage()); - } - } catch (ClientException $e) { - if ($e->getResponse()->getStatusCode() === 403) { - $this->logger->critical('Your DeepL API credentials are either wrong, or you don\'t have access to the requested API.'); - } elseif ($e->getResponse()->getStatusCode() === 429) { - $this->logger->warning('You sent too many requests to the DeepL API, we\'ll retry to connect to the API on the next request'); - } elseif ($e->getResponse()->getStatusCode() === 456) { - $this->logger->warning('You reached your DeepL API character limit. Upgrade your plan or wait until your quota is filled up again.'); - } elseif ($e->getResponse()->getStatusCode() === 400) { - $this->logger->warning('Your DeepL API request was not well-formed. Please check the source and the target language in particular.', [ - 'sourceLanguage' => $sourceLanguage, - 'targetLanguage' => $targetLanguage - ]); - } else { - $this->logger->warning('The DeepL API request did not complete successfully, see status code and message below.', [ - 'statusCode' => $e->getResponse()->getStatusCode(), - 'message' => $e->getResponse()->getBody()->getContents() - ]); - } - - // If the call went wrong, return the original text - $translatedText = $text; - } catch (GuzzleException $e) { + } else { $this->logger->warning('The DeepL API request did not complete successfully, see status code and message below.', [ 'statusCode' => $e->getResponse()->getStatusCode(), 'message' => $e->getResponse()->getBody()->getContents() ]); - - // If the call went wrong, return the original text - $translatedText = $text; } + + // If the call went wrong, return the original text + $translatedText = $text; + } catch (GuzzleException $e) { + $this->logger->warning('The DeepL API request did not complete successfully, see status code and message below.', [ + 'statusCode' => $e->getResponse()->getStatusCode(), + 'message' => $e->getResponse()->getBody()->getContents() + ]); + + // If the call went wrong, return the original text + $translatedText = $text; } return $translatedText; diff --git a/Classes/Domain/Service/NodeTranslationService.php b/Classes/Domain/Service/NodeTranslationService.php index 9447a63..5cb7bef 100644 --- a/Classes/Domain/Service/NodeTranslationService.php +++ b/Classes/Domain/Service/NodeTranslationService.php @@ -35,7 +35,7 @@ public function afterAdoptNode(NodeInterface $node, Context $context, $recursive $sourceLanguage = explode('_', $node->getContext()->getTargetDimensions()['language'])[0]; $targetLanguage = explode('_', $context->getTargetDimensions()['language'])[0]; - foreach ($node->getProperties() as $propertyName => $propertyValue) { + foreach ($adoptedNode->getProperties() as $propertyName => $propertyValue) { if (empty($propertyValue)) { continue; diff --git a/Classes/EelHelper/TranslationHelper.php b/Classes/EelHelper/TranslationHelper.php index 9a00501..078dbc9 100644 --- a/Classes/EelHelper/TranslationHelper.php +++ b/Classes/EelHelper/TranslationHelper.php @@ -3,6 +3,7 @@ namespace CodeQ\DeepLTranslationHelper\EelHelper; use CodeQ\DeepLTranslationHelper\Domain\Service\DeepLService; +use Neos\Cache\Frontend\VariableFrontend; use Neos\Flow\Annotations as Flow; use Neos\Eel\ProtectedContextAwareInterface; @@ -14,6 +15,12 @@ class TranslationHelper implements ProtectedContextAwareInterface { */ protected $deepLService; + /** + * @Flow\Inject + * @var VariableFrontend + */ + protected $translationCache; + /** * @param string $text * @param string $targetLanguage @@ -23,7 +30,14 @@ class TranslationHelper implements ProtectedContextAwareInterface { */ public function translate(string $text, string $targetLanguage, string $sourceLanguage = null): string { - return $this->deepLService->translate($text, $targetLanguage, $sourceLanguage); + // See: https://ideone.com/embed/0iwuGn + $cacheIdentifier = sprintf('%s-%s-%s', hash('haval256,3', $text), $sourceLanguage, $targetLanguage); + if ($translatedText = $this->translationCache->get($cacheIdentifier)) { + return $translatedText; + } + $translatedText = $this->deepLService->translate($text, $targetLanguage, $sourceLanguage); + $this->translationCache->set($cacheIdentifier, $translatedText); + return $translatedText; } /** diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml index 5da1046..8f427f9 100644 --- a/Configuration/Objects.yaml +++ b/Configuration/Objects.yaml @@ -1,4 +1,4 @@ -CodeQ\DeepLTranslationHelper\Domain\Service\DeepLService: +CodeQ\DeepLTranslationHelper\EelHelper\TranslationHelper: properties: translationCache: object: From eac360de23db2a1665ebbe098fd91600ad228e19 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 29 Sep 2021 22:19:11 +0200 Subject: [PATCH 3/4] TASK: Adjust translation api to support the translation of multiple fragments with a single request. --- Classes/Domain/Service/DeepLService.php | 127 +++++++++--------- .../Domain/Service/NodeTranslationService.php | 23 +++- Classes/EelHelper/TranslationHelper.php | 4 +- Configuration/NodeTypes.yaml | 11 ++ Configuration/Settings.yaml | 15 ++- README.md | 26 +++- composer.json | 3 +- 7 files changed, 135 insertions(+), 74 deletions(-) create mode 100644 Configuration/NodeTypes.yaml diff --git a/Classes/Domain/Service/DeepLService.php b/Classes/Domain/Service/DeepLService.php index 3953817..1fddca9 100644 --- a/Classes/Domain/Service/DeepLService.php +++ b/Classes/Domain/Service/DeepLService.php @@ -3,9 +3,6 @@ namespace CodeQ\DeepLTranslationHelper\Domain\Service; use Neos\Flow\Annotations as Flow; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\GuzzleException; use Psr\Log\LoggerInterface; /** @@ -13,10 +10,6 @@ */ class DeepLService { - /** - * @var Client|null - */ - protected ?Client $deeplClient = null; /** * @var array @@ -30,80 +23,86 @@ class DeepLService */ protected $logger; - protected function initializeObject() - { - $this->deeplClient = new Client([ - 'base_uri' => $this->settings['baseUri'], - 'timeout' => 0, - 'headers' => [ - 'Authorization' => sprintf('DeepL-Auth-Key %s', $this->settings['apiAuthKey']) - ] - ]); - } - /** - * @param string $text - * @param string $targetLanguage - * + * @param string[] $texts + * @param string $targetLanguage * @param string|null $sourceLanguage - * - * @return string + * @return array */ - public function translate( - string $text, - string $targetLanguage, - string $sourceLanguage = null - ): string { - if ($sourceLanguage === $targetLanguage) { - return $text; + public function translate(array $texts, string $targetLanguage, ?string $sourceLanguage = null): array + { + $keys = array_keys($texts); + $values = array_values($texts); + + $baseUri = $this->settings['useFreeApi'] ? $this->settings['baseUriFree'] : $this->settings['baseUri']; + + $curlHandle = curl_init($baseUri . 'translate'); + curl_setopt($curlHandle, CURLOPT_TIMEOUT, 0); + curl_setopt($curlHandle, CURLOPT_HTTPHEADER, ['Expect:']); + curl_setopt($curlHandle, CURLOPT_POST, true); + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_setopt($curlHandle, CURLOPT_HTTPHEADER, [ + 'Accept: */*', + 'Content-Type: application/x-www-form-urlencoded', + sprintf('Authorization: DeepL-Auth-Key %s', $this->settings['apiAuthKey']) + ]); + + // create request body ... neither psr nor guzzle can create the body format that + // is required here + $body = http_build_query($this->settings['defaultOptions']); + if ($sourceLanguage) { + $body .= '&source_lang=' . urlencode($sourceLanguage); } + $body .= '&target_lang=' . urlencode($targetLanguage); - try { - $response = $this->deeplClient->get('translate', [ - 'query' => [ - 'text' => $text, - 'source_lang' => $sourceLanguage, - 'target_lang' => $targetLanguage, - 'tag_handling' => 'xml', - 'split_sentences' => 'nonewlines' - ] - ]); - - $responseBody = json_decode($response->getBody()->getContents(), - true); - $translations = $responseBody['translations']; - $translatedText = $translations[0]['text']; - } catch (ClientException $e) { - if ($e->getResponse()->getStatusCode() === 403) { + foreach($values as $part) { + $body .= '&text=' . urlencode($part); + } + + curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); + + // return + $curlResult = curl_exec($curlHandle); + if ($curlResult === false) { + return $texts; + } + + $status = curl_getinfo($curlHandle, CURLINFO_RESPONSE_CODE); + + if ($status != 200) { + if ($status === 403) { $this->logger->critical('Your DeepL API credentials are either wrong, or you don\'t have access to the requested API.'); - } elseif ($e->getResponse()->getStatusCode() === 429) { + } elseif ($status === 429) { $this->logger->warning('You sent too many requests to the DeepL API, we\'ll retry to connect to the API on the next request'); - } elseif ($e->getResponse()->getStatusCode() === 456) { + } elseif ($status === 456) { $this->logger->warning('You reached your DeepL API character limit. Upgrade your plan or wait until your quota is filled up again.'); - } elseif ($e->getResponse()->getStatusCode() === 400) { + } elseif ($status === 400) { $this->logger->warning('Your DeepL API request was not well-formed. Please check the source and the target language in particular.', [ 'sourceLanguage' => $sourceLanguage, 'targetLanguage' => $targetLanguage ]); } else { - $this->logger->warning('The DeepL API request did not complete successfully, see status code and message below.', [ - 'statusCode' => $e->getResponse()->getStatusCode(), - 'message' => $e->getResponse()->getBody()->getContents() - ]); + $this->logger->warning('Unexpected status from Deepl API', ['status' => $status]); } + return $texts; + } - // If the call went wrong, return the original text - $translatedText = $text; - } catch (GuzzleException $e) { - $this->logger->warning('The DeepL API request did not complete successfully, see status code and message below.', [ - 'statusCode' => $e->getResponse()->getStatusCode(), - 'message' => $e->getResponse()->getBody()->getContents() - ]); + curl_close($curlHandle); - // If the call went wrong, return the original text - $translatedText = $text; + $returnedData = json_decode($curlResult, true); + + if (is_null($returnedData)) { + return $texts; } - return $translatedText; + $translations = array_map( + function($part) { + return $part['text']; + }, + $returnedData['translations'] + ); + + return array_combine($keys, $translations); } } diff --git a/Classes/Domain/Service/NodeTranslationService.php b/Classes/Domain/Service/NodeTranslationService.php index 5cb7bef..81a4984 100644 --- a/Classes/Domain/Service/NodeTranslationService.php +++ b/Classes/Domain/Service/NodeTranslationService.php @@ -5,6 +5,7 @@ use Neos\Flow\Annotations as Flow; use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\ContentRepository\Domain\Service\Context; +use Psr\Log\LoggerInterface; class NodeTranslationService { @@ -16,7 +17,13 @@ class NodeTranslationService protected $deeplService; /** - * @Flow\InjectConfiguration(path="translateRichtextProperties") + * @Flow\Inject + * @var LoggerInterface + */ + protected $logger; + + /** + * @Flow\InjectConfiguration(path="nodeTranslations.translateInlineEditables") * @var bool */ protected $translateRichtextProperties; @@ -35,6 +42,7 @@ public function afterAdoptNode(NodeInterface $node, Context $context, $recursive $sourceLanguage = explode('_', $node->getContext()->getTargetDimensions()['language'])[0]; $targetLanguage = explode('_', $context->getTargetDimensions()['language'])[0]; + $propertiesToTranslate = []; foreach ($adoptedNode->getProperties() as $propertyName => $propertyValue) { if (empty($propertyValue)) { @@ -46,10 +54,13 @@ public function afterAdoptNode(NodeInterface $node, Context $context, $recursive if ($propertyDefinitions[$propertyName]['type'] != 'string' || !is_string($propertyValue)) { continue; } + if ((trim(strip_tags($propertyValue))) == "") { + continue; + } $translateProperty = false; $isInlineEditable = $propertyDefinitions[$propertyName]['ui']['inlineEditable'] ?? false; - $isTranslateEnabled = $propertyDefinitions[$propertyName]['options']['autotranslate'] ?? false; + $isTranslateEnabled = $propertyDefinitions[$propertyName]['options']['deeplTranslate'] ?? false; if ($this->translateRichtextProperties && $isInlineEditable == true) { $translateProperty = true; } @@ -58,7 +69,13 @@ public function afterAdoptNode(NodeInterface $node, Context $context, $recursive } if ($translateProperty) { - $translatedValue = $this->deeplService->translate($propertyValue, $targetLanguage, $sourceLanguage); + $propertiesToTranslate[$propertyName] = $propertyValue; + } + } + + if (count($propertiesToTranslate) > 0) { + $translatedProperties = $this->deeplService->translate($propertiesToTranslate, $targetLanguage, $sourceLanguage); + foreach($translatedProperties as $propertyName => $translatedValue) { $adoptedNode->setProperty($propertyName, $translatedValue); } } diff --git a/Classes/EelHelper/TranslationHelper.php b/Classes/EelHelper/TranslationHelper.php index 078dbc9..ce97852 100644 --- a/Classes/EelHelper/TranslationHelper.php +++ b/Classes/EelHelper/TranslationHelper.php @@ -35,8 +35,8 @@ public function translate(string $text, string $targetLanguage, string $sourceLa if ($translatedText = $this->translationCache->get($cacheIdentifier)) { return $translatedText; } - $translatedText = $this->deepLService->translate($text, $targetLanguage, $sourceLanguage); - $this->translationCache->set($cacheIdentifier, $translatedText); + $translatedTexts = $this->deepLService->translate(['text' => $text], $targetLanguage, $sourceLanguage); + $this->translationCache->set($cacheIdentifier, $translatedTexts['text']); return $translatedText; } diff --git a/Configuration/NodeTypes.yaml b/Configuration/NodeTypes.yaml new file mode 100644 index 0000000..6d3079c --- /dev/null +++ b/Configuration/NodeTypes.yaml @@ -0,0 +1,11 @@ +'Neos.Neos:Document': + properties: + title: + options: + deeplTranslate: true + titleOverride: + options: + deeplTranslate: true + metaDescription: + options: + deeplTranslate: true diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 3f1ffea..c2e2e34 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,9 +1,20 @@ CodeQ: DeepLTranslationHelper: DeepLService: - baseUri: 'https://api.deepl.com/v2/' + useFreeApi: false apiAuthKey: '' - translateRichtextProperties: true + + baseUri: 'https://api.deepl.com/v2/' + baseUriFree: 'https://api-free.deepl.com/v2/' + + defaultOptions: + tag_handling: 'xml' + split_sentences: 'nonewlines' + preserve_formatting: 1 + formality: "default" + + nodeTranslations: + translateInlineEditables: true Neos: Fusion: diff --git a/README.md b/README.md index 9850f97..85962aa 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you are using the free API, you need to change the baseUri: CodeQ: DeepLTranslationHelper: DeepLService: - baseUri: 'https://api-free.deepl.com/v2/' + useFreeApi: true apiAuthKey: 'myapikey' ``` @@ -50,3 +50,27 @@ CodeQ_DeepLTranslationHelper_Translation: backendOptions: defaultLifetime: 2592000 ``` + + +## Node Translations + +When nodes are copied (adopted) into another languge the fields can be translated automatically. + +The following setting enables the translation of all inlineEditable properties. + +```yaml +CodeQ: + DeepLTranslationHelper: + nodeTranslations: + translateInlineEditables: true +``` + +Other properties of type string can be translated aswell with the following configuration. + + ```yaml +Neos.Neos:Document: + properties: + title: + options: + deeplTranslate: true +``` diff --git a/composer.json b/composer.json index cdeb45d..250f9cf 100644 --- a/composer.json +++ b/composer.json @@ -3,8 +3,7 @@ "type": "neos-package", "name": "codeq/deepltranslationhelper", "require": { - "neos/flow": "*", - "guzzlehttp/guzzle": "^7.0" + "neos/flow": "*" }, "autoload": { "psr-4": { From e2c963e8a5c48120b25e998f4265d34d22f9ed40 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Tue, 5 Oct 2021 19:32:07 +0200 Subject: [PATCH 4/4] TASK: Use PSR7 and flow httpFactories This avoids the direct dependency to guzzle but still uses psr factories and requests as much as possible. Only the http timeout is set as curl option. --- Classes/Domain/Service/DeepLService.php | 102 +++++++++++++----------- composer.json | 3 +- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/Classes/Domain/Service/DeepLService.php b/Classes/Domain/Service/DeepLService.php index 1fddca9..6e228b5 100644 --- a/Classes/Domain/Service/DeepLService.php +++ b/Classes/Domain/Service/DeepLService.php @@ -3,6 +3,11 @@ namespace CodeQ\DeepLTranslationHelper\Domain\Service; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Http\Client\Browser; +use Neos\Flow\Http\Client\CurlEngine; +use Neos\Http\Factories\ServerRequestFactory; +use Neos\Http\Factories\StreamFactory; +use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; /** @@ -23,6 +28,18 @@ class DeepLService */ protected $logger; + /** + * @Flow\Inject + * @var ServerRequestFactory + */ + protected $serverRequestFactory; + + /** + * @Flow\Inject + * @var StreamFactory + */ + protected $streamFactory; + /** * @param string[] $texts * @param string $targetLanguage @@ -31,78 +48,67 @@ class DeepLService */ public function translate(array $texts, string $targetLanguage, ?string $sourceLanguage = null): array { + // store keys and values seperately for later reunion $keys = array_keys($texts); $values = array_values($texts); $baseUri = $this->settings['useFreeApi'] ? $this->settings['baseUriFree'] : $this->settings['baseUri']; - $curlHandle = curl_init($baseUri . 'translate'); - curl_setopt($curlHandle, CURLOPT_TIMEOUT, 0); - curl_setopt($curlHandle, CURLOPT_HTTPHEADER, ['Expect:']); - curl_setopt($curlHandle, CURLOPT_POST, true); - curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_setopt($curlHandle, CURLOPT_HTTPHEADER, [ - 'Accept: */*', - 'Content-Type: application/x-www-form-urlencoded', - sprintf('Authorization: DeepL-Auth-Key %s', $this->settings['apiAuthKey']) - ]); - - // create request body ... neither psr nor guzzle can create the body format that - // is required here + // request body ... this has to be done manually because of the non php ish format + // with multiple text arguments $body = http_build_query($this->settings['defaultOptions']); if ($sourceLanguage) { $body .= '&source_lang=' . urlencode($sourceLanguage); } $body .= '&target_lang=' . urlencode($targetLanguage); - foreach($values as $part) { $body .= '&text=' . urlencode($part); } - curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); - - // return - $curlResult = curl_exec($curlHandle); - if ($curlResult === false) { - return $texts; - } - - $status = curl_getinfo($curlHandle, CURLINFO_RESPONSE_CODE); - - if ($status != 200) { - if ($status === 403) { + $apiRequest = $this->serverRequestFactory->createServerRequest('POST', $baseUri . 'translate') + ->withHeader('Accept', 'application/json') + ->withHeader('Authorization', sprintf('DeepL-Auth-Key %s', $this->settings['apiAuthKey'])) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody($this->streamFactory->createStream($body)); + + $browser = new Browser(); + $engine = new CurlEngine(); + $engine->setOption(CURLOPT_TIMEOUT, 0); + $browser->setRequestEngine($engine); + + /** + * @var ResponseInterface $apiResponse + */ + $apiResponse = $browser->sendRequest($apiRequest); + + if ($apiResponse->getStatusCode() == 200) { + $returnedData = json_decode($apiResponse->getBody()->getContents(), true); + if (is_null($returnedData)) { + return $texts; + } + $translations = array_map( + function($part) { + return $part['text']; + }, + $returnedData['translations'] + ); + return array_combine($keys, $translations); + } else { + if ($apiResponse->getStatusCode() === 403) { $this->logger->critical('Your DeepL API credentials are either wrong, or you don\'t have access to the requested API.'); - } elseif ($status === 429) { + } elseif ($apiResponse->getStatusCode() === 429) { $this->logger->warning('You sent too many requests to the DeepL API, we\'ll retry to connect to the API on the next request'); - } elseif ($status === 456) { + } elseif ($apiResponse->getStatusCode() === 456) { $this->logger->warning('You reached your DeepL API character limit. Upgrade your plan or wait until your quota is filled up again.'); - } elseif ($status === 400) { + } elseif ($apiResponse->getStatusCode() === 400) { $this->logger->warning('Your DeepL API request was not well-formed. Please check the source and the target language in particular.', [ 'sourceLanguage' => $sourceLanguage, 'targetLanguage' => $targetLanguage ]); } else { - $this->logger->warning('Unexpected status from Deepl API', ['status' => $status]); + $this->logger->warning('Unexpected status from Deepl API', ['status' => $apiResponse->getStatusCode()]); } return $texts; } - - curl_close($curlHandle); - - $returnedData = json_decode($curlResult, true); - - if (is_null($returnedData)) { - return $texts; - } - - $translations = array_map( - function($part) { - return $part['text']; - }, - $returnedData['translations'] - ); - - return array_combine($keys, $translations); } } diff --git a/composer.json b/composer.json index 250f9cf..80e265e 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "type": "neos-package", "name": "codeq/deepltranslationhelper", "require": { - "neos/flow": "*" + "neos/flow": "*", + "neos/http-factories": "*" }, "autoload": { "psr-4": {