src/Products/NotificationsBundle/Service/Notifications/TranslationService.php line 81

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Service\Notifications;
  3. use App\Service\Intl\CloudTranslator;
  4. use App\Util\Locales;
  5. use Cms\CoreBundle\Util\Doctrine\EntityManager;
  6. use Platform\QueueBundle\Event\AsyncEvent;
  7. use Platform\QueueBundle\Model\AsyncMessage;
  8. use Platform\QueueBundle\Service\AsyncQueueService;
  9. use Products\NotificationsBundle\Doctrine\Repository\RecordingRepository;
  10. use Products\NotificationsBundle\Entity\AbstractNotification;
  11. use Products\NotificationsBundle\Entity\Notifications\Channels\ChannelsInterface;
  12. use Products\NotificationsBundle\Entity\Notifications\Invocation;
  13. use Products\NotificationsBundle\Entity\Notifications\Message;
  14. use Products\NotificationsBundle\Entity\Notifications\Translations\Translation;
  15. use Products\NotificationsBundle\Entity\NotificationsConfig;
  16. use Products\NotificationsBundle\Entity\Recording;
  17. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  18. use Doctrine\ORM\ORMException;
  19. use Doctrine\ORM\OptimisticLockException;
  20. use Doctrine\ORM\NonUniqueResultException;
  21. final class TranslationService implements EventSubscriberInterface
  22. {
  23.     private const TRANSLATION_EVENT_MAPPINGS = [
  24.         Message::class => self::EVENTS__TRANSLATE__MESSAGE,
  25.         Invocation::class => self::EVENTS__TRANSLATE__INVOCATION,
  26.     ];
  27.     public const EVENTS__TRANSLATE__MESSAGE 'app.notifications.translations.message';
  28.     public const EVENTS__TRANSLATE__INVOCATION 'app.notifications.translations.invocation';
  29.     private NotificationsConfigService $notificationsConfigService;
  30.     private EntityManager $em;
  31.     private AsyncQueueService $asyncQueueService;
  32.     private CloudTranslator $cloudTranslator;
  33.     /**
  34.      * @param NotificationsConfigService $notificationsConfigService
  35.      * @param AsyncQueueService $asyncQueueService
  36.      * @param EntityManager $em
  37.      * @param CloudTranslator $cloudTranslator
  38.      */
  39.     public function __construct(
  40.         NotificationsConfigService $notificationsConfigService,
  41.         EntityManager $em,
  42.         AsyncQueueService $asyncQueueService,
  43.         CloudTranslator $cloudTranslator
  44.     )
  45.     {
  46.         $this->notificationsConfigService $notificationsConfigService;
  47.         $this->em $em;
  48.         $this->asyncQueueService $asyncQueueService;
  49.         $this->cloudTranslator $cloudTranslator;
  50.     }
  51.     /**
  52.      * {@inheritDoc}
  53.      */
  54.     public static function getSubscribedEvents(): array
  55.     {
  56.         return [
  57.             self::EVENTS__TRANSLATE__MESSAGE => ['onTranslateMessage'0],
  58.         ];
  59.     }
  60.     /**
  61.      * @return array
  62.      */
  63.     public function getTranslationLocales(): array
  64.     {
  65.         $notificationsConfig $this->notificationsConfigService->getNotificationsConfig();
  66.         return ($notificationsConfig instanceof NotificationsConfig) ? $notificationsConfig->getTranslationLocales() : [];
  67.     }
  68.     /**
  69.      * @param AbstractNotification $notification
  70.      * @param bool $force
  71.      * @return void
  72.      */
  73.     public function translate(AbstractNotification $notificationbool $force true): void
  74.     {
  75.         // need to loop over all the possible locales as there could be some missing from the current set
  76.         foreach ($this->getTranslationLocales() as $locale) {
  77.             // get the event
  78.             $event null;
  79.             foreach (self::TRANSLATION_EVENT_MAPPINGS as $class => $evt) {
  80.                 if ($notification instanceof $class) {
  81.                     $event $evt;
  82.                     break;
  83.                 }
  84.             }
  85.             if ( ! $event) {
  86.                 throw new \LogicException();
  87.             }
  88.             $shouldTranslate false;
  89.             $translation $notification->getTranslation($locale);
  90.             if (($translation instanceof Translation) && ($force || !$translation->hasFlag(Translation::FLAGS__MANUAL))) {
  91.                 // TODO: only reset translation if there were real changes made to the message?
  92.                 $this->resetTranslation($translation);
  93.                 $shouldTranslate true;
  94.             } elseif ( ! ($translation instanceof Translation)) {
  95.                 $shouldTranslate true;
  96.             }
  97.             if ( ! $shouldTranslate) {
  98.                 continue;
  99.             }
  100.             // perform an async translation
  101.             $this->asyncQueueService->send(
  102.                 null,
  103.                 new AsyncMessage(
  104.                     $notification,
  105.                     $event,
  106.                     [
  107.                         'id' => $notification->getId(),
  108.                         'locale' => $locale,
  109.                         'force' => $force,
  110.                     ],
  111.                     AsyncMessage::PRIORITY__CRITICAL,
  112.                 ),
  113.             );
  114.         }
  115.     }
  116.     /**
  117.      * @param Translation $translation
  118.      * @return void
  119.      * @throws ORMException
  120.      * @throws OptimisticLockException
  121.      */
  122.     private function resetTranslation(Translation $translation)
  123.     {
  124.         $translation->setTitle(null);
  125.         $translation->setDescription(null);
  126.         $translation->setScript(null);
  127.         $translation->setRecording(null);
  128.         $this->em->persist($translation);
  129.         $this->em->flush();
  130.     }
  131.     /**
  132.      * TODO: how do we handle translation failures???
  133.      *
  134.      * @param AsyncEvent $event
  135.      * @return void
  136.      */
  137.     public function onTranslateMessage(AsyncEvent $event): void
  138.     {
  139.         // get vars in the event
  140.         $id $event->getBody()->get('id');
  141.         $locale $event->getBody()->get('locale');
  142.         $force $event->getBody()->get('force');
  143.         // find the message
  144.         $message $this->em->getRepository(Message::class)->find($id);
  145.         if ( ! $message instanceof Message) {
  146.             throw new \RuntimeException();
  147.         }
  148.         // see if there is a translation for this message and this locale
  149.         $translation $message->getTranslation($locale);
  150.         // if we don't have one, need to make one
  151.         $translation $translation ?: (new Translation())
  152.             ->setTenant($message->getTenant())
  153.             ->setLocale($locale);
  154.         // attach it to the message
  155.         $message->addTranslation($translation);
  156.         $message->unmarkFlag(AbstractNotification::FLAGS__MODIFIED_SINCE_TRANSLATION);
  157.         // save translation to the database
  158.         $this->em->transactional(
  159.             function (EntityManager $em) use ($translation$message) {
  160.                 $em->saveAll(
  161.                     [
  162.                         $translation,
  163.                         $message,
  164.                     ],
  165.                 );
  166.             },
  167.         );
  168.         // running these separate to get more real-time processing stats on the waiting page...
  169.         $this->em->save(
  170.             $translation->unmarkFlag(Translation::FLAGS__MANUAL)
  171.         );
  172.         $this->em->save(
  173.             $translation->setTitle(
  174.                 $this->cloudTranslator->translateText(
  175.                     Locales::RFC4646_DEFAULT,
  176.                     $locale,
  177.                     $message->getTitle(),
  178.                 ) ?: null,
  179.             ),
  180.         );
  181.         $this->em->save(
  182.             $translation->setDescription(
  183.                 $this->cloudTranslator->translateHtml(
  184.                     Locales::RFC4646_DEFAULT,
  185.                     $locale,
  186.                     $message->getDescription(),
  187.                 ) ?: null,
  188.             ),
  189.         );
  190.         if ($message->hasChannel(ChannelsInterface::CHANNELS__VOICE)) {
  191.             $this->em->save(
  192.                 $translation->setScript(
  193.                     $tts $this->cloudTranslator->translateText(
  194.                         Locales::RFC4646_DEFAULT,
  195.                         $locale,
  196.                         $message->getScriptAsPlainText(),
  197.                     ) ?: null,
  198.                 ),
  199.             );
  200.             $this->em->save(
  201.                 $translation->setRecording(
  202.                     $tts $this->cloudTranslator->transcribe(
  203.                         $locale,
  204.                         $tts,
  205.                     ) : null,
  206.                 ),
  207.             );
  208.         } else {
  209.             $this->em->save(
  210.                 $translation
  211.                     ->setScript(null)
  212.                     ->setRecording(null)
  213.             );
  214.         }
  215.     }
  216.     /**
  217.      * @param Translation $translation
  218.      * @return bool
  219.      * @throws NonUniqueResultException
  220.      */
  221.     public function isModified(Translation $translation): bool
  222.     {
  223.         return (
  224.             $this->isPropertyModified('title'$translation) ||
  225.             $this->isPropertyModified('description'$translation) ||
  226.             $this->isPropertyModified('script'$translation) ||
  227.             $this->isRecordingModified($translation)
  228.         );
  229.     }
  230.     /**
  231.      * @param Translation $translation
  232.      * @return bool
  233.      * @throws NonUniqueResultException
  234.      */
  235.     public function isRecordingModified(Translation $translation): bool
  236.     {
  237.         /** @var RecordingRepository $recordingRepository */
  238.         $recordingRepository $this->em->getRepository(Recording::class);
  239.         $existingRecording $recordingRepository->findOneByTranslation($translation);
  240.         $isRecordingModified false;
  241.         if (($translation->getRecording() instanceof Recording) && !empty($translation->getRecording()->getId())) {
  242.             $isRecordingModified = ( !($existingRecording instanceof Recording) || ($existingRecording->getId() !== $translation->getRecording()->getId()) );
  243.         }
  244.         return $isRecordingModified;
  245.     }
  246.     /**
  247.      * @param string $propertyName
  248.      * @param Translation $translation
  249.      * @return bool
  250.      */
  251.     public function isPropertyModified(string $propertyNameTranslation $translation): bool
  252.     {
  253.         $originalTranslation $this->em->getUnitOfWork()->getOriginalEntityData($translation);
  254.         switch ($propertyName) {
  255.             case 'title':
  256.                 if (isset($originalTranslation['title']) && $this->isPropertyValueModified($originalTranslation['title'], $translation->getTitle())) {
  257.                     return true;
  258.                 }
  259.                 break;
  260.             case 'description':
  261.                 if (isset($originalTranslation['description']) && $this->isPropertyValueModified($originalTranslation['description'], $translation->getDescription())) {
  262.                     return true;
  263.                 }
  264.                 break;
  265.             case 'script':
  266.                 if (isset($originalTranslation['script']) && $this->isPropertyValueModified($originalTranslation['script'], $translation->getScript())) {
  267.                     return true;
  268.                 }
  269.                 break;
  270.             default:
  271.                 throw new \LogicException();
  272.         }
  273.         return false;
  274.     }
  275.     /**
  276.      * @param string|null $originalValue
  277.      * @param string|null $currentValue
  278.      * @return bool
  279.      */
  280.     private function isPropertyValueModified(?string $originalValue, ?string $currentValue): bool
  281.     {
  282.         $strippedOriginalValue trim(preg_replace('/\s+/'' '$originalValue));
  283.         $strippedCurrentValue trim(preg_replace('/\s+/'' '$currentValue));
  284.         return ($strippedOriginalValue !== $strippedCurrentValue);
  285.     }
  286. }