Skip to content
This repository has been archived by the owner on Jan 10, 2023. It is now read-only.

FEATURE: Translate CR Nodes automatically. #1

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 74 additions & 91 deletions Classes/Domain/Service/DeepLService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,19 @@

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;

/**
* @Flow\Scope("singleton")
*/
class DeepLService
{
/**
* @var Client|null
*/
protected ?Client $deeplClient = null;

/**
* @var array
Expand All @@ -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;
}
}
83 changes: 83 additions & 0 deletions Classes/Domain/Service/NodeTranslationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace CodeQ\DeepLTranslationHelper\Domain\Service;

use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Service\Context;
use Psr\Log\LoggerInterface;

class NodeTranslationService
{

/**
* @Flow\Inject
* @var DeepLService
*/
protected $deeplService;

/**
* @Flow\Inject
* @var LoggerInterface
*/
protected $logger;

/**
* @Flow\InjectConfiguration(path="nodeTranslations.translateInlineEditables")
* @var bool
*/
protected $translateRichtextProperties;

/**
* @param NodeInterface $node
* @param Context $context
* @param $recursive
* @return void
*/
public function afterAdoptNode(NodeInterface $node, Context $context, $recursive)
{
$propertyDefinitions = $node->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);
}
}
}
}
16 changes: 15 additions & 1 deletion Classes/EelHelper/TranslationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -14,6 +15,12 @@ class TranslationHelper implements ProtectedContextAwareInterface {
*/
protected $deepLService;

/**
* @Flow\Inject
* @var VariableFrontend
*/
protected $translationCache;

/**
* @param string $text
* @param string $targetLanguage
Expand All @@ -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;
}

/**
Expand Down
23 changes: 23 additions & 0 deletions Classes/Package.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
namespace CodeQ\DeepLTranslationHelper;

use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Package\Package as BasePackage;
use Neos\ContentRepository\Domain\Service\Context;
use CodeQ\DeepLTranslationHelper\Domain\Service\NodeTranslationService;

/**
* The Neos Package
*/
class Package extends BasePackage
{
/**
* @param Bootstrap $bootstrap The current bootstrap
* @return void
*/
public function boot(Bootstrap $bootstrap)
{
$dispatcher = $bootstrap->getSignalSlotDispatcher();
$dispatcher->connect(Context::class, 'afterAdoptNode', NodeTranslationService::class, 'afterAdoptNode');
}
}
11 changes: 11 additions & 0 deletions Configuration/NodeTypes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'Neos.Neos:Document':
properties:
title:
options:
deeplTranslate: true
titleOverride:
options:
deeplTranslate: true
metaDescription:
options:
deeplTranslate: true
2 changes: 1 addition & 1 deletion Configuration/Objects.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CodeQ\DeepLTranslationHelper\Domain\Service\DeepLService:
CodeQ\DeepLTranslationHelper\EelHelper\TranslationHelper:
properties:
translationCache:
object:
Expand Down
14 changes: 13 additions & 1 deletion Configuration/Settings.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
Loading