diff --git a/Classes/Domain/Service/DeepLService.php b/Classes/Domain/Service/DeepLService.php index d38448c..6e228b5 100644 --- a/Classes/Domain/Service/DeepLService.php +++ b/Classes/Domain/Service/DeepLService.php @@ -2,12 +2,12 @@ namespace CodeQ\DeepLTranslationHelper\Domain\Service; -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 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; /** @@ -15,10 +15,6 @@ */ class DeepLService { - /** - * @var Client|null - */ - protected ?Client $deeplClient = null; /** * @var array @@ -27,105 +23,92 @@ class DeepLService protected array $settings; /** - * @var VariableFrontend + * @Flow\Inject + * @var LoggerInterface */ - protected $translationCache; + protected $logger; /** * @Flow\Inject - * @var LoggerInterface + * @var ServerRequestFactory */ - protected $logger; + protected $serverRequestFactory; - protected function initializeObject() - { - $this->deeplClient = new Client([ - 'base_uri' => $this->settings['baseUri'], - 'timeout' => 0, - 'headers' => [ - 'Authorization' => sprintf('DeepL-Auth-Key %s', $this->settings['apiAuthKey']) - ] - ]); - } + /** + * @Flow\Inject + * @var StreamFactory + */ + protected $streamFactory; /** - * @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 + { + // 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']; + + // 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); } - // See: https://ideone.com/embed/0iwuGn - $cacheIdentifier = sprintf('%s-%s', hash('haval256,3', $text), - $targetLanguage); - $translatedText = $this->translationCache->get($cacheIdentifier); + $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)); - if ($translatedText === false) { - try { - $response = $this->deeplClient->get('translate', [ - 'query' => [ - 'text' => $text, - 'source_lang' => $sourceLanguage, - 'target_lang' => $targetLanguage, - 'tag_handling' => 'xml', - 'split_sentences' => 'nonewlines' - ] - ]); + $browser = new Browser(); + $engine = new CurlEngine(); + $engine->setOption(CURLOPT_TIMEOUT, 0); + $browser->setRequestEngine($engine); - $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() - ]); - } + /** + * @var ResponseInterface $apiResponse + */ + $apiResponse = $browser->sendRequest($apiRequest); - // 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 ($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 ($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 ($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 ($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 ]); - - // If the call went wrong, return the original text - $translatedText = $text; + } else { + $this->logger->warning('Unexpected status from Deepl API', ['status' => $apiResponse->getStatusCode()]); } + return $texts; } - - return $translatedText; } } diff --git a/Classes/Domain/Service/NodeTranslationService.php b/Classes/Domain/Service/NodeTranslationService.php new file mode 100644 index 0000000..81a4984 --- /dev/null +++ b/Classes/Domain/Service/NodeTranslationService.php @@ -0,0 +1,83 @@ +getNodeType()->getProperties(); + $adoptedNode = $context->getNodeByIdentifier($node->getIdentifier()); + + $sourceLanguage = explode('_', $node->getContext()->getTargetDimensions()['language'])[0]; + $targetLanguage = explode('_', $context->getTargetDimensions()['language'])[0]; + + $propertiesToTranslate = []; + foreach ($adoptedNode->getProperties() as $propertyName => $propertyValue) { + + if (empty($propertyValue)) { + continue; + } + if (!array_key_exists($propertyName, $propertyDefinitions)) { + continue; + } + 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']['deeplTranslate'] ?? false; + if ($this->translateRichtextProperties && $isInlineEditable == true) { + $translateProperty = true; + } + if ($isTranslateEnabled) { + $translateProperty = true; + } + + if ($translateProperty) { + $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 9a00501..ce97852 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; + } + $translatedTexts = $this->deepLService->translate(['text' => $text], $targetLanguage, $sourceLanguage); + $this->translationCache->set($cacheIdentifier, $translatedTexts['text']); + return $translatedText; } /** 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/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/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: diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 1b0de68..c2e2e34 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,9 +1,21 @@ CodeQ: DeepLTranslationHelper: DeepLService: - baseUri: 'https://api.deepl.com/v2/' + useFreeApi: false apiAuthKey: '' + 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: defaultContext: 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 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": {