<?php
namespace Products\NotificationsBundle\Service\Notifications;
use App\Service\Intl\CloudTranslator;
use App\Util\Locales;
use Cms\CoreBundle\Util\Doctrine\EntityManager;
use Platform\QueueBundle\Event\AsyncEvent;
use Platform\QueueBundle\Model\AsyncMessage;
use Platform\QueueBundle\Service\AsyncQueueService;
use Products\NotificationsBundle\Doctrine\Repository\RecordingRepository;
use Products\NotificationsBundle\Entity\AbstractNotification;
use Products\NotificationsBundle\Entity\Notifications\Channels\ChannelsInterface;
use Products\NotificationsBundle\Entity\Notifications\Invocation;
use Products\NotificationsBundle\Entity\Notifications\Message;
use Products\NotificationsBundle\Entity\Notifications\Translations\Translation;
use Products\NotificationsBundle\Entity\NotificationsConfig;
use Products\NotificationsBundle\Entity\Recording;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\NonUniqueResultException;
final class TranslationService implements EventSubscriberInterface
{
private const TRANSLATION_EVENT_MAPPINGS = [
Message::class => self::EVENTS__TRANSLATE__MESSAGE,
Invocation::class => self::EVENTS__TRANSLATE__INVOCATION,
];
public const EVENTS__TRANSLATE__MESSAGE = 'app.notifications.translations.message';
public const EVENTS__TRANSLATE__INVOCATION = 'app.notifications.translations.invocation';
private NotificationsConfigService $notificationsConfigService;
private EntityManager $em;
private AsyncQueueService $asyncQueueService;
private CloudTranslator $cloudTranslator;
/**
* @param NotificationsConfigService $notificationsConfigService
* @param AsyncQueueService $asyncQueueService
* @param EntityManager $em
* @param CloudTranslator $cloudTranslator
*/
public function __construct(
NotificationsConfigService $notificationsConfigService,
EntityManager $em,
AsyncQueueService $asyncQueueService,
CloudTranslator $cloudTranslator
)
{
$this->notificationsConfigService = $notificationsConfigService;
$this->em = $em;
$this->asyncQueueService = $asyncQueueService;
$this->cloudTranslator = $cloudTranslator;
}
/**
* {@inheritDoc}
*/
public static function getSubscribedEvents(): array
{
return [
self::EVENTS__TRANSLATE__MESSAGE => ['onTranslateMessage', 0],
];
}
/**
* @return array
*/
public function getTranslationLocales(): array
{
$notificationsConfig = $this->notificationsConfigService->getNotificationsConfig();
return ($notificationsConfig instanceof NotificationsConfig) ? $notificationsConfig->getTranslationLocales() : [];
}
/**
* @param AbstractNotification $notification
* @param bool $force
* @return void
*/
public function translate(AbstractNotification $notification, bool $force = true): void
{
// need to loop over all the possible locales as there could be some missing from the current set
foreach ($this->getTranslationLocales() as $locale) {
// get the event
$event = null;
foreach (self::TRANSLATION_EVENT_MAPPINGS as $class => $evt) {
if ($notification instanceof $class) {
$event = $evt;
break;
}
}
if ( ! $event) {
throw new \LogicException();
}
$shouldTranslate = false;
$translation = $notification->getTranslation($locale);
if (($translation instanceof Translation) && ($force || !$translation->hasFlag(Translation::FLAGS__MANUAL))) {
// TODO: only reset translation if there were real changes made to the message?
$this->resetTranslation($translation);
$shouldTranslate = true;
} elseif ( ! ($translation instanceof Translation)) {
$shouldTranslate = true;
}
if ( ! $shouldTranslate) {
continue;
}
// perform an async translation
$this->asyncQueueService->send(
null,
new AsyncMessage(
$notification,
$event,
[
'id' => $notification->getId(),
'locale' => $locale,
'force' => $force,
],
AsyncMessage::PRIORITY__CRITICAL,
),
);
}
}
/**
* @param Translation $translation
* @return void
* @throws ORMException
* @throws OptimisticLockException
*/
private function resetTranslation(Translation $translation)
{
$translation->setTitle(null);
$translation->setDescription(null);
$translation->setScript(null);
$translation->setRecording(null);
$this->em->persist($translation);
$this->em->flush();
}
/**
* TODO: how do we handle translation failures???
*
* @param AsyncEvent $event
* @return void
*/
public function onTranslateMessage(AsyncEvent $event): void
{
// get vars in the event
$id = $event->getBody()->get('id');
$locale = $event->getBody()->get('locale');
$force = $event->getBody()->get('force');
// find the message
$message = $this->em->getRepository(Message::class)->find($id);
if ( ! $message instanceof Message) {
throw new \RuntimeException();
}
// see if there is a translation for this message and this locale
$translation = $message->getTranslation($locale);
// if we don't have one, need to make one
$translation = $translation ?: (new Translation())
->setTenant($message->getTenant())
->setLocale($locale);
// attach it to the message
$message->addTranslation($translation);
$message->unmarkFlag(AbstractNotification::FLAGS__MODIFIED_SINCE_TRANSLATION);
// save translation to the database
$this->em->transactional(
function (EntityManager $em) use ($translation, $message) {
$em->saveAll(
[
$translation,
$message,
],
);
},
);
// running these separate to get more real-time processing stats on the waiting page...
$this->em->save(
$translation->unmarkFlag(Translation::FLAGS__MANUAL)
);
$this->em->save(
$translation->setTitle(
$this->cloudTranslator->translateText(
Locales::RFC4646_DEFAULT,
$locale,
$message->getTitle(),
) ?: null,
),
);
$this->em->save(
$translation->setDescription(
$this->cloudTranslator->translateHtml(
Locales::RFC4646_DEFAULT,
$locale,
$message->getDescription(),
) ?: null,
),
);
if ($message->hasChannel(ChannelsInterface::CHANNELS__VOICE)) {
$this->em->save(
$translation->setScript(
$tts = $this->cloudTranslator->translateText(
Locales::RFC4646_DEFAULT,
$locale,
$message->getScriptAsPlainText(),
) ?: null,
),
);
$this->em->save(
$translation->setRecording(
$tts ? $this->cloudTranslator->transcribe(
$locale,
$tts,
) : null,
),
);
} else {
$this->em->save(
$translation
->setScript(null)
->setRecording(null)
);
}
}
/**
* @param Translation $translation
* @return bool
* @throws NonUniqueResultException
*/
public function isModified(Translation $translation): bool
{
return (
$this->isPropertyModified('title', $translation) ||
$this->isPropertyModified('description', $translation) ||
$this->isPropertyModified('script', $translation) ||
$this->isRecordingModified($translation)
);
}
/**
* @param Translation $translation
* @return bool
* @throws NonUniqueResultException
*/
public function isRecordingModified(Translation $translation): bool
{
/** @var RecordingRepository $recordingRepository */
$recordingRepository = $this->em->getRepository(Recording::class);
$existingRecording = $recordingRepository->findOneByTranslation($translation);
$isRecordingModified = false;
if (($translation->getRecording() instanceof Recording) && !empty($translation->getRecording()->getId())) {
$isRecordingModified = ( !($existingRecording instanceof Recording) || ($existingRecording->getId() !== $translation->getRecording()->getId()) );
}
return $isRecordingModified;
}
/**
* @param string $propertyName
* @param Translation $translation
* @return bool
*/
public function isPropertyModified(string $propertyName, Translation $translation): bool
{
$originalTranslation = $this->em->getUnitOfWork()->getOriginalEntityData($translation);
switch ($propertyName) {
case 'title':
if (isset($originalTranslation['title']) && $this->isPropertyValueModified($originalTranslation['title'], $translation->getTitle())) {
return true;
}
break;
case 'description':
if (isset($originalTranslation['description']) && $this->isPropertyValueModified($originalTranslation['description'], $translation->getDescription())) {
return true;
}
break;
case 'script':
if (isset($originalTranslation['script']) && $this->isPropertyValueModified($originalTranslation['script'], $translation->getScript())) {
return true;
}
break;
default:
throw new \LogicException();
}
return false;
}
/**
* @param string|null $originalValue
* @param string|null $currentValue
* @return bool
*/
private function isPropertyValueModified(?string $originalValue, ?string $currentValue): bool
{
$strippedOriginalValue = trim(preg_replace('/\s+/', ' ', $originalValue));
$strippedCurrentValue = trim(preg_replace('/\s+/', ' ', $currentValue));
return ($strippedOriginalValue !== $strippedCurrentValue);
}
}