diff --git a/bundle/Command/TranslateContentCommand.php b/bundle/Command/TranslateContentCommand.php
index 77d486c..49ee5aa 100644
--- a/bundle/Command/TranslateContentCommand.php
+++ b/bundle/Command/TranslateContentCommand.php
@@ -8,48 +8,77 @@
namespace EzSystems\EzPlatformAutomatedTranslationBundle\Command;
+use Doctrine\ORM\EntityManagerInterface;
+use \Exception;
+use EzSystems\EzPlatformAutomatedTranslationBundle\Entity\AutoTranslationActions;
+use EzSystems\EzPlatformAutomatedTranslationBundle\Handler\AutoTranslationActionsHandler;
use Ibexa\Contracts\Core\Repository\ContentService;
+use Ibexa\Contracts\Core\Repository\LocationService;
+use Ibexa\Contracts\Core\Repository\Exceptions\BadStateException;
+use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException;
+use Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
+use Ibexa\Contracts\Core\Repository\Repository;
use Ibexa\Contracts\Core\Repository\UserService;
use EzSystems\EzPlatformAutomatedTranslation\ClientProvider;
use EzSystems\EzPlatformAutomatedTranslation\Translator;
+use Ibexa\Contracts\Core\Repository\Values\Content\Content;
+use Pagerfanta\Doctrine\DBAL\QueryAdapter;
+use Pagerfanta\Pagerfanta;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
+use Ibexa\Migration\Log\LoggerAwareTrait;
+use RuntimeException;
+use Ibexa\Core\Base\Exceptions\InvalidArgumentException;
+use Symfony\Component\Console\Command\LockableTrait;
+use Symfony\Component\Process\Process;
+use Pagerfanta\Adapter\CallbackAdapter;
final class TranslateContentCommand extends Command
{
- private const ADMINISTRATOR_USER_ID = 14;
-
- /** @var Translator */
- private $translator;
-
- /** @var ClientProvider */
- private $clientProvider;
+ use LockableTrait;
+ use LoggerAwareTrait;
- /** @var \Ibexa\Contracts\Core\Repository\ContentService */
- private $contentService;
-
- /** @var \Ibexa\Contracts\Core\Repository\PermissionResolver */
- private $permissionResolver;
+ private const ADMINISTRATOR_USER_ID = 14;
- /** @var \Ibexa\Contracts\Core\Repository\UserService */
- private $userService;
+ protected EntityManagerInterface $em;
+ protected Translator $translator;
+ protected ClientProvider $clientProvider;
+ protected ContentService $contentService;
+ protected LocationService $locationService;
+ protected PermissionResolver $permissionResolver;
+ protected UserService $userService;
+ protected Repository $repository;
+ protected AutoTranslationActionsHandler $handler;
public function __construct(
+ EntityManagerInterface $em,
Translator $translator,
ClientProvider $clientProvider,
ContentService $contentService,
+ LocationService $locationService,
PermissionResolver $permissionResolver,
- UserService $userService
+ UserService $userService,
+ Repository $repository,
+ AutoTranslationActionsHandler $handler,
+ ?LoggerInterface $logger = null
) {
+ $this->em = $em;
$this->clientProvider = $clientProvider;
$this->translator = $translator;
$this->contentService = $contentService;
+ $this->locationService = $locationService;
$this->permissionResolver = $permissionResolver;
$this->userService = $userService;
+ $this->repository = $repository;
+ $this->handler = $handler;
+ $this->logger = $logger ?? new NullLogger();
parent::__construct();
}
@@ -60,38 +89,318 @@ protected function configure(): void
->setName('ezplatform:automated:translate')
->setAliases(['eztranslate'])
->setDescription('Translate a Content in a new Language')
- ->addArgument('contentId', InputArgument::REQUIRED, 'ContentId')
->addArgument(
'service',
InputArgument::REQUIRED,
'Remote Service for Translation.
';
+
+ $adapter = new CallbackAdapter(
+ function () use ($subtreeLocation): int {
+ return $this->handler->countContentWithRelationsInSubtree($subtreeLocation->pathString);
+ },
+ function (int $offset, int $limit) use ($subtreeLocation): iterable {
+ return $this->handler->getContentsWithRelationsInSubtree($subtreeLocation->pathString, $offset, $limit);
+ }
+ );
+ $currentPage = 1;
+ $maxPerPage = 1;
+ $pager = new Pagerfanta(
+ $adapter
+ );
+ $pager->setMaxPerPage($maxPerPage);
+ $pager->setCurrentPage($currentPage);
+ $i = 0;
+ $message = sprintf('Translate "%s" contents.', $pager->count());
+ $this->getLogger()->info($message);
+ $output->writeln($message);
+ $logMessage .= $message .'
';
+
+ $progressBar = new ProgressBar($output, $pager->count());
+
+ if ($pager->count() > 0) {
+ do {
+ $i++;
+ $adapter = new CallbackAdapter(
+ function () use ($subtreeLocation): int {
+ return $this->handler->countContentWithRelationsInSubtree($subtreeLocation->pathString);
+ },
+ function (int $offset, int $limit) use ($subtreeLocation): iterable {
+ return $this->handler->getContentsWithRelationsInSubtree($subtreeLocation->pathString, $offset, $limit);
+ }
+ );
+ $pager = new Pagerfanta(
+ $adapter
+ );
+ $pager->setMaxPerPage($maxPerPage);
+ $pager->setCurrentPage($currentPage);
+ $contentIds = [];
+ /** @var Content $content */
+ foreach ($pager->getCurrentPageResults() as $result) {
+ $contentIds[] = $result['contentId'];
+ }
+ $processes = $this->getPhpProcess(
+ $contentIds[0],
+ $targetLanguage,
+ $userId,
+ $overwrite,
+ $input
+ );
+ $processes->run();
+ $processes->getErrorOutput();
+ $logMessage .= $processes->getOutput() .'
';
+ if (!empty($processes->getErrorOutput())) {
+ $message = $processes->getErrorOutput();
+ $this->getLogger()->info($message);
+ $logMessage .= $message .'
';
+ } else {
+ $logMessage .= $processes->getOutput() .'
';
+ }
+ $currentPage++;
+ $progressBar->advance($maxPerPage);
+ } while ($pager->hasNextPage() && $i < 2000);
+ $progressBar->finish();
+ }
+ // clear leftover progress bar parts
+ $progressBar->clear();
+ } catch (Exception $e) {
+ $logMessage = $e->getMessage();
+ $this->logException($e);
+ $cmdStatus = Command::FAILURE;
+ }
+
+ $autoTranslationActions->setStatus($cmdStatus === Command::FAILURE ?
+ AutoTranslationActions::STATUS_FAILED : AutoTranslationActions::STATUS_FINISHED
+ );
+ $autoTranslationActions->setLogMessage($logMessage);
+ $autoTranslationActions->setFinishedAt(new \DateTime());
+ $this->em->flush($autoTranslationActions);
+ }
+ $output->writeln('');
+ $output->writeln('Finished');
+ $this->getLogger()->info('Finished');
+ $output->writeln('');
+ }
+
+ return $cmdStatus;
+ }
+ protected function subExecute(InputInterface $input, OutputInterface $output): int
{
$contentId = (int) $input->getArgument('contentId');
+ $languageCodeFrom = $input->getOption('from');
+ $languageCodeTo = $input->getOption('to');
+ $serviceApi = $input->getArgument('service');
+ $overwrite = $input->getOption('overwrite');
+
+ $status = Command::SUCCESS;
+ if ($input->getOption('sudo')) {
+ $status = $this->permissionResolver->sudo(function () use (
+ $contentId,
+ $languageCodeFrom,
+ $languageCodeTo,
+ $serviceApi,
+ $overwrite,
+ $input,
+ $output
+ ) {
+ try {
+ $this->publishVersion($contentId, $languageCodeFrom, $languageCodeTo, $serviceApi, $overwrite);
+ $this->logDoneMessage($contentId, $languageCodeFrom, $languageCodeTo, $output);
+ return Command::SUCCESS;
+ } catch (Exception $e) {
+ $this->logFailedMessage($contentId, $e);
+ return Command::FAILURE;
+ }
+ }, $this->repository);
+ } else {
+ try {
+ $this->publishVersion($contentId, $languageCodeFrom, $languageCodeTo, $serviceApi, $overwrite);
+ $this->logDoneMessage($contentId, $languageCodeFrom, $languageCodeTo, $output);
+ } catch (\Throwable $e) {
+ $this->logFailedMessage($contentId, $e);
+ $status = Command::FAILURE;
+ }
+ }
+
+ return $status;
+ }
+
+ /**
+ * @throws BadStateException
+ * @throws NotFoundException
+ * @throws UnauthorizedException
+ * @throws InvalidArgumentException
+ */
+ protected function publishVersion(
+ int $contentId,
+ ?string &$languageCodeFrom,
+ string $languageCodeTo,
+ string $serviceApi,
+ bool $overwrite = false
+ ): array {
$content = $this->contentService->loadContent($contentId);
+ $languageCodeFrom = $languageCodeFrom ?? $content->contentInfo->mainLanguageCode;
+
+ if($languageCodeFrom == $languageCodeTo) {
+ $message = sprintf('The target language from argument --to=%s must be different from the source language --from and the main language of the content %s .',
+ $languageCodeTo,
+ $languageCodeFrom,
+ );
+ throw new InvalidArgumentException('--from', $message);
+ }
+
+ if(!$overwrite && in_array($languageCodeTo,$content->getVersionInfo()->languageCodes)) {
+ $message = sprintf('The content %d already translated into language --to=%s . use the --overwrite option. ',
+ $contentId,
+ $languageCodeTo
+ );
+ throw new InvalidArgumentException('--to', $message);
+ }
+
$draft = $this->translator->getTranslatedContent(
- $input->getOption('from'),
- $input->getOption('to'),
- $input->getArgument('service'),
+ $languageCodeFrom,
+ $languageCodeTo,
+ $serviceApi,
$content
);
- $this->contentService->publishVersion($draft->versionInfo);
- $output->writeln("Translation to {$contentId} Done.");
+ $newContentVersion = $this->contentService->publishVersion($draft->versionInfo);
- return Command::SUCCESS;
+ return [$content, $newContentVersion];
}
+ protected function logDoneMessage(
+ int $contentId,
+ string $languageCodeFrom,
+ string $languageCodeTo,
+ OutputInterface $output
+ ): void
+ {
+ $message = sprintf(
+ 'Translation of content %d from %s to %s Done.',
+ $contentId,
+ $languageCodeFrom,
+ $languageCodeTo
+ );
+ $this->getLogger()->info($message);
+ $output->writeln($message);
+ }
+ protected function logFailedMessage(int $contentId, Exception $e): void
+ {
+ $message = sprintf(
+ 'Translation to %d Failed',
+ $contentId
+ );
+ $this->logException($e, $message);
+ }
+ protected function logException(Exception $e, string $message = ''): void
+ {
+ $message = sprintf(
+ '%s. %s',
+ $message,
+ $e->getMessage(),
+ );
+ $exception = new RuntimeException($message, $e->getCode(), $e);
+ $this->getLogger()->error($message, [
+ 'exception' => $exception,
+ ]);
+ }
protected function initialize(InputInterface $input, OutputInterface $output): void
{
parent::initialize($input, $output);
+ $userId = (int) ($input->getOption('user') ?? self::ADMINISTRATOR_USER_ID);
+
$this->permissionResolver->setCurrentUserReference(
- $this->userService->loadUser(self::ADMINISTRATOR_USER_ID)
+ $this->userService->loadUser($userId)
);
}
+
+ private function getPhpProcess(int $contentId, string $targetLanguage, int $userId, bool $overwrite, InputInterface $input): Process
+ {
+ $env = $input->getParameterOption(['--env', '-e'], getenv('APP_ENV') ?: 'dev', true);
+ $serviceApi = $input->getArgument('service');
+ $sudo = $input->getOption('sudo');
+
+ $subProcessArgs = [
+ 'php',
+ 'bin/console',
+ $this->getName(),
+ $serviceApi,
+ $contentId,
+ '--user=' . $userId,
+ '--to=' . $targetLanguage,
+ '--env=' . $env
+ ];
+
+ if ($overwrite) {
+ $subProcessArgs[] = '--overwrite';
+ }
+
+ if ($sudo) {
+ $subProcessArgs[] = '--sudo';
+ }
+
+ $process = new Process($subProcessArgs);
+ $process->setTimeout(null);
+
+ return $process;
+ }
}
diff --git a/bundle/Controller/BaseController.php b/bundle/Controller/BaseController.php
new file mode 100644
index 0000000..93cd1ba
--- /dev/null
+++ b/bundle/Controller/BaseController.php
@@ -0,0 +1,87 @@
+permissionResolver = $permissionResolver;
+ }
+
+ /**
+ * @required
+ */
+ public function setTranslator(TranslatorInterface $translator): void
+ {
+ $this->translator = $translator;
+ }
+
+ /**
+ * @required
+ */
+ public function setNotificationHandler(NotificationHandlerInterface $notificationHandler): void
+ {
+ $this->notificationHandler = $notificationHandler;
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ protected function permissionAccess(string $module, string $function)
+ {
+ if (!$this->permissionResolver->hasAccess($module, $function)) {
+ $exception = $this->createAccessDeniedException($this->translator->trans(
+ 'auto_translation.permission.failed'
+ ));
+ $exception->setAttributes(null);
+ $exception->setSubject(null);
+
+ throw $exception;
+ }
+
+ return null;
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ protected function permissionManageAccess(string $module, array $functions): array
+ {
+ $access = [];
+ foreach ($functions as $function) {
+ $access[$function] = true;
+ if (!$this->permissionResolver->hasAccess($module, $function)) {
+ $access[$function] = false;
+ }
+ }
+
+ return $access;
+ }
+}
diff --git a/bundle/Controller/TranslationSubtreeController.php b/bundle/Controller/TranslationSubtreeController.php
new file mode 100644
index 0000000..ab2b8c7
--- /dev/null
+++ b/bundle/Controller/TranslationSubtreeController.php
@@ -0,0 +1,113 @@
+permissionAccess('auto_translation', 'view');
+
+ $formSearch = $this->createForm(AutoTranslationActionsSearchType::class , null, [
+ 'action' => $this->generateUrl('automated_translation_index'),
+ 'method' => 'GET',
+ ]);
+
+ $formSearch->handleRequest($request);
+ $page = $request->query->get('page' ,1);
+
+ $adapter = new QueryAdapter(
+ $handler->getAllQuery($formSearch->getData()['sort'] ?? []),
+ function ($queryBuilder) use ($handler) {
+ return $handler->countAll($queryBuilder);
+ });
+ $pagerfanta = new Pagerfanta(
+ $adapter
+ );
+
+ $pagerfanta->setMaxPerPage($this->defaultPaginationLimit);
+ $pagerfanta->setCurrentPage($page);
+
+ $autoTranslationActions = new AutoTranslationActions();
+ $form = $this->createForm(AutoTranslationActionsType::class, $autoTranslationActions, [
+ 'action' => $this->generateUrl('automated_translation_add'),
+ 'method' => 'POST',
+ ]);
+
+ return $this->render( '@ibexadesign/auto_translation/view.html.twig', [
+ 'form' => $form->createView(),
+ 'form_search' => $formSearch->createView(),
+ 'pager' => $pagerfanta,
+ 'canCreate' => $this->permissionResolver->hasAccess('auto_translation', 'create'),
+ 'canDelete' => $this->permissionResolver->hasAccess('auto_translation', 'delete'),
+ ]);
+ }
+ /**
+ * @Route("/add", name="automated_translation_add")
+ */
+ public function addAction(
+ Request $request,
+ EntityManagerInterface $em,
+ LocationService $locationService,
+ TranslatableNotificationHandlerInterface $notificationHandler
+ ): RedirectResponse {
+ $this->permissionAccess('auto_translation', 'create');
+
+ $autoTranslationActions = new AutoTranslationActions();
+ $form = $this->createForm(AutoTranslationActionsType::class, $autoTranslationActions);
+ $form->handleRequest($request);
+ /**
+ * @var User $user
+ */
+ $user = $this->getUser();
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $autoTranslationActions->setStatus(AutoTranslationActions::STATUS_PENDING);
+ $autoTranslationActions->setUserId($user->getAPIUser()->id);
+ $em->persist($autoTranslationActions);
+ $em->flush();
+ try {
+ $location = $locationService->loadLocation($autoTranslationActions->getSubtreeId());
+ $this->notificationHandler->success($this->translator->trans(
+ 'auto_translation.add.success' ,['%subtree_name%' => $location->contentInfo->name ]
+ ));
+ } catch (NotFoundException|UnauthorizedException $e) {
+ $this->notificationHandler->error($e->getMessage());
+ }
+ }
+
+ return new RedirectResponse($this->generateUrl(
+ 'automated_translation_index'
+ ));
+ }
+}
diff --git a/bundle/DependencyInjection/Configuration.php b/bundle/DependencyInjection/Configuration.php
index 59b720a..5392c0b 100644
--- a/bundle/DependencyInjection/Configuration.php
+++ b/bundle/DependencyInjection/Configuration.php
@@ -8,7 +8,7 @@
namespace EzSystems\EzPlatformAutomatedTranslationBundle\DependencyInjection;
-use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\SiteAccessAware;
+use Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class Configuration extends SiteAccessAware\Configuration
@@ -22,8 +22,8 @@ public function getConfigTreeBuilder(): TreeBuilder
->variableNode('configurations')->end()
->arrayNode('non_translatable_characters')->end()
->arrayNode('non_translatable_tags')->end()
- ->arrayNode('non_translatable_self_closed_tags')->end();
-
+ ->arrayNode('non_translatable_self_closed_tags')->end()
+ ->arrayNode('exclude_attribute')->end();
return $treeBuilder;
}
}
diff --git a/bundle/DependencyInjection/EzPlatformAutomatedTranslationExtension.php b/bundle/DependencyInjection/EzPlatformAutomatedTranslationExtension.php
index 12bdbd7..ca8cdc7 100644
--- a/bundle/DependencyInjection/EzPlatformAutomatedTranslationExtension.php
+++ b/bundle/DependencyInjection/EzPlatformAutomatedTranslationExtension.php
@@ -8,15 +8,18 @@
namespace EzSystems\EzPlatformAutomatedTranslationBundle\DependencyInjection;
-use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\SiteAccessAware\ConfigurationProcessor;
+use Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware\ConfigurationProcessor;
use EzSystems\EzPlatformAutomatedTranslation\Encoder\BlockAttribute\BlockAttributeEncoderInterface;
use EzSystems\EzPlatformAutomatedTranslation\Encoder\Field\FieldEncoderInterface;
use Symfony\Component\Config\FileLocator;
+use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\Yaml\Yaml;
-class EzPlatformAutomatedTranslationExtension extends Extension
+class EzPlatformAutomatedTranslationExtension extends Extension implements PrependExtensionInterface
{
/**
* {@inheritdoc}
@@ -27,8 +30,6 @@ public function load(array $configs, ContainerBuilder $container): void
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
- // always needed because of Bundle extension.
- $loader->load('services_override.yml');
$container->registerForAutoconfiguration(FieldEncoderInterface::class)
->addTag('ezplatform.automated_translation.field_encoder');
@@ -44,9 +45,10 @@ public function load(array $configs, ContainerBuilder $container): void
return;
}
- $loader->load('ezadminui.yml');
$loader->load('default_settings.yml');
$loader->load('services.yml');
+ // always needed because of Bundle extension.
+ $loader->load('services_override.yml');
$processor = new ConfigurationProcessor($container, $this->getAlias());
$processor->mapSetting('configurations', $config);
@@ -66,4 +68,21 @@ private function hasConfiguredClients(array $config, ContainerBuilder $container
});
}));
}
+
+ /**
+ * Allow an extension to prepend the extension configurations.
+ */
+ public function prepend(ContainerBuilder $container): void
+ {
+ $configs = [
+ 'universal_discovery_widget.yaml' => 'ibexa',
+ ];
+
+ foreach ($configs as $fileName => $extensionName) {
+ $configFile = __DIR__.'/../Resources/config/'.$fileName;
+ $config = Yaml::parse(file_get_contents($configFile));
+ $container->prependExtensionConfig($extensionName, $config);
+ $container->addResource(new FileResource($configFile));
+ }
+ }
}
diff --git a/bundle/DependencyInjection/Security/Provider/AutoTranslationPolicyProvider.php b/bundle/DependencyInjection/Security/Provider/AutoTranslationPolicyProvider.php
new file mode 100644
index 0000000..66e807c
--- /dev/null
+++ b/bundle/DependencyInjection/Security/Provider/AutoTranslationPolicyProvider.php
@@ -0,0 +1,26 @@
+id;
+ }
+
+ public function getSubtreeId(): int
+ {
+ return $this->subtreeId;
+ }
+
+ public function setSubtreeId(int $subtreeId): self
+ {
+ $this->subtreeId = $subtreeId;
+ return $this;
+ }
+
+ public function getUserId(): int
+ {
+ return $this->userId;
+ }
+
+ public function setUserId(int $userId): self
+ {
+ $this->userId = $userId;
+ return $this;
+ }
+
+ public function getTargetLanguage(): string
+ {
+ return $this->targetLanguage;
+ }
+
+ public function setTargetLanguage(string $targetLanguage): self
+ {
+ $this->targetLanguage = $targetLanguage;
+ return $this;
+ }
+
+ public function isOverwrite(): bool
+ {
+ return $this->overwrite;
+ }
+
+ public function setOverwrite(bool $overwrite): self
+ {
+ $this->overwrite = $overwrite;
+ return $this;
+ }
+
+ public function getStatus(): string
+ {
+ return $this->status;
+ }
+
+ public function setStatus(string $status): self
+ {
+ $this->status = $status;
+ return $this;
+ }
+
+ public function setCreatedAt(DateTime $createdAt): self
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?DateTime
+ {
+ return $this->createdAt;
+ }
+
+ /**
+ * @return DateTime|null
+ */
+ public function getFinishedAt(): ?DateTime
+ {
+ return $this->finishedAt;
+ }
+
+ /**
+ * @param DateTime|null $finishedAt
+ */
+ public function setFinishedAt(?DateTime $finishedAt = null): void
+ {
+ $this->finishedAt = $finishedAt;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getLogMessage(): ?string
+ {
+ return $this->logMessage;
+ }
+
+ /**
+ * @param string|null $logMessage
+ */
+ public function setLogMessage(?string $logMessage): void
+ {
+ $this->logMessage = $logMessage;
+ }
+
+ /**
+ * @ORM\PrePersist
+ */
+ public function updatedTimestamps(): void
+ {
+ $dateTimeNow = new DateTime('now');
+ if ($this->getCreatedAt() === null) {
+ $this->setCreatedAt($dateTimeNow);
+ }
+ }
+}
diff --git a/bundle/Event/FieldEncodeEvent.php b/bundle/Event/FieldEncodeEvent.php
index 6104c38..d74c299 100644
--- a/bundle/Event/FieldEncodeEvent.php
+++ b/bundle/Event/FieldEncodeEvent.php
@@ -12,7 +12,7 @@
final class FieldEncodeEvent
{
- /** @var \eZ\Publish\API\Repository\Values\Content\Field */
+ /** @var Field */
private $field;
/** @var string */
diff --git a/bundle/Event/RichTextDecodeEvent.php b/bundle/Event/RichTextDecodeEvent.php
new file mode 100644
index 0000000..f7b277f
--- /dev/null
+++ b/bundle/Event/RichTextDecodeEvent.php
@@ -0,0 +1,24 @@
+xmlValue;
+ }
+
+ public function setValue(string $value): void
+ {
+ $this->xmlValue = $value;
+ }
+}
diff --git a/bundle/Event/RichTextEncodeEvent.php b/bundle/Event/RichTextEncodeEvent.php
new file mode 100644
index 0000000..53bc043
--- /dev/null
+++ b/bundle/Event/RichTextEncodeEvent.php
@@ -0,0 +1,24 @@
+xmlValue;
+ }
+
+ public function setValue(string $value): void
+ {
+ $this->xmlValue = $value;
+ }
+}
diff --git a/bundle/EventListener/ContentProxyTranslateListener.php b/bundle/EventListener/ContentProxyTranslateListener.php
index 10c5f1f..dc73ac2 100644
--- a/bundle/EventListener/ContentProxyTranslateListener.php
+++ b/bundle/EventListener/ContentProxyTranslateListener.php
@@ -18,16 +18,16 @@
class ContentProxyTranslateListener implements EventSubscriberInterface
{
- /** @var \Symfony\Component\HttpFoundation\RequestStack */
+ /** @var RequestStack */
private $requestStack;
- /** @var \EzSystems\EzPlatformAutomatedTranslation\Translator */
+ /** @var Translator */
private $translator;
- /** @var \Ibexa\Contracts\Core\Repository\ContentService */
+ /** @var ContentService */
private $contentService;
- /** @var \Symfony\Component\Routing\RouterInterface */
+ /** @var RouterInterface */
private $router;
public function __construct(
diff --git a/bundle/EventListener/MenuSubscriber.php b/bundle/EventListener/MenuSubscriber.php
new file mode 100644
index 0000000..5df77f4
--- /dev/null
+++ b/bundle/EventListener/MenuSubscriber.php
@@ -0,0 +1,46 @@
+permissionResolver = $permissionResolver;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ ConfigureMenuEvent::MAIN_MENU => ['onMainMenuConfigure', 0],
+ ];
+ }
+
+ public function onMainMenuConfigure(ConfigureMenuEvent $event): void
+ {
+ $menu = $event->getMenu();
+ if ($this->permissionResolver->hasAccess('auto_translation', 'view')) {
+ $menu[MainMenuBuilder::ITEM_CONTENT]->addChild(
+ 'content_translation_translations_list',
+ [
+ 'label' => 'content_translation',
+ 'route' => 'automated_translation_index',
+ ]
+ );
+ }
+ }
+}
diff --git a/bundle/Events.php b/bundle/Events.php
index 1628efc..fab8f42 100644
--- a/bundle/Events.php
+++ b/bundle/Events.php
@@ -19,4 +19,14 @@ final class Events
* @Event("\EzSystems\EzPlatformAutomatedTranslationBundle\Event\FieldDecodeEvent")
*/
const POST_FIELD_DECODE = 'ez_automated_translation.post_field_decode';
+
+ /**
+ * @Event("\EzSystems\EzPlatformAutomatedTranslationBundle\Event\RichTextEncodeEvent")
+ */
+ const PRE_RICHTEXT_ENCODE = 'ez_automated_translation.pre_richtext_encode';
+
+ /**
+ * @Event("\EzSystems\EzPlatformAutomatedTranslationBundle\Event\RichTextDecodeEvent")
+ */
+ const PRE_RICHTEXT_DECODE = 'ez_automated_translation.pre_richtext_decode';
}
diff --git a/bundle/EzPlatformAutomatedTranslationBundle.php b/bundle/EzPlatformAutomatedTranslationBundle.php
index 5646e0f..11e2f53 100644
--- a/bundle/EzPlatformAutomatedTranslationBundle.php
+++ b/bundle/EzPlatformAutomatedTranslationBundle.php
@@ -8,6 +8,7 @@
namespace EzSystems\EzPlatformAutomatedTranslationBundle;
+use EzSystems\EzPlatformAutomatedTranslationBundle\DependencyInjection\Security\Provider\AutoTranslationPolicyProvider;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -21,5 +22,7 @@ public function getParent(): ?string
public function build(ContainerBuilder $container): void
{
parent::build($container);
+ $ibexaExtension = $container->getExtension('ibexa');
+ $ibexaExtension->addPolicyProvider(new AutoTranslationPolicyProvider());
}
}
diff --git a/bundle/Form/AutoTranslationActionsSearchType.php b/bundle/Form/AutoTranslationActionsSearchType.php
new file mode 100644
index 0000000..35ba88d
--- /dev/null
+++ b/bundle/Form/AutoTranslationActionsSearchType.php
@@ -0,0 +1,49 @@
+add('sort', SortType::class, [
+ 'row_attr' => [
+ 'hidden' => 'hidden'
+ ],
+ 'sort_fields' => ['created_at', 'user_name', 'content_name', 'target_language' ,'overwrite' ,'status'],
+ 'default' => ['field' => 'created_at', 'direction' => '0'],
+ ])
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults(
+ [
+ 'attr' => ['__template' => self::TEMPLATE],
+ 'method' => Request::METHOD_GET,
+ 'csrf_protection' => false,
+ ]
+ );
+ }
+}
diff --git a/bundle/Form/AutoTranslationActionsType.php b/bundle/Form/AutoTranslationActionsType.php
new file mode 100755
index 0000000..cd9c117
--- /dev/null
+++ b/bundle/Form/AutoTranslationActionsType.php
@@ -0,0 +1,162 @@
+em = $em;
+ $this->locationService = $locationService;
+ $this->languageService = $languageService;
+ $this->translator = $translator;
+ $this->notificationHandler = $notificationHandler;
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $languages = $this->languageService->loadLanguages();
+ $defaultLanguage = new Language([
+ 'languageCode' => '',
+ 'name' => 'auto_translation.form.target_language.default_language'
+ ]);
+
+ $builder
+ ->add(
+ 'subtree_id',
+ SubtreeLocationType::class,
+ [
+ 'compound' => true,
+ 'label' => 'auto_translation.form.subtree_id.label',
+ 'row_attr' => [
+ 'class' => 'ibexa-field-edit auto-translation-field--ezobjectrelationlist'
+ ] ,
+ 'attr' => [
+ 'class' => 'btn-udw-trigger ibexa-button-tree pure-button
+ ibexa-font-icon ibexa-btn btn ibexa-btn--secondary
+ js-auto_translation-select-location-id'
+ ],
+ 'empty_data' => [],
+ ]
+ )
+ ->add(
+ 'target_language',
+ ChoiceType::class,
+ [
+ 'required' => true,
+ 'compound' => false,
+ 'choices' => [...[$defaultLanguage], ...$languages],
+ 'setter' => function (AutoTranslationActions $autoTranslationActions, ?Language $language, FormInterface $form): void {
+ $autoTranslationActions->setTargetLanguage($language->getLanguageCode());
+ },
+ 'choice_value' => function (?Language $language): string {
+ return $language ? $language->getLanguageCode() : '';
+ },
+ 'choice_label' => function (?Language $language): string {
+ return $language ? $this->translator->trans($language->getName()) : '';
+ },
+ 'label' => 'auto_translation.form.target_language.label',
+ 'row_attr' => [
+ 'class' => 'ibexa-field-edit auto-translation-field--ezselection'
+ ],
+ 'attr' => [
+ 'class' => 'ibexa-data-source__input ibexa-data-source__input--selection ibexa-input ibexa-input--select form-select'
+ ],
+ ]
+ )
+ ->add(
+ 'overwrite',
+ CheckboxType::class,
+
+ [
+ 'required' => false,
+ 'label' => 'auto_translation.form.overwrite.label'
+ ]
+ )
+ ->add(
+ 'submit',
+ SubmitType::class,
+ [
+ 'label' => false
+ ]
+ )
+ ->addEventListener(FormEvents::POST_SUBMIT, function (PostSubmitEvent $event) {
+ /** @var Form $form */
+ $form = $event->getForm();
+ /** @var AutoTranslationActions $autoTranslationActions */
+ $autoTranslationActions = $form->getData();
+ $count = $this->em->getRepository(AutoTranslationActions::class)->count([
+ 'subtreeId' => $autoTranslationActions->getSubtreeId(),
+ 'targetLanguage' => $autoTranslationActions->getTargetLanguage(),
+ 'status' => [AutoTranslationActions::STATUS_PENDING, AutoTranslationActions::STATUS_IN_PROGRESS],
+ ]);
+ if ($count) {
+ try {
+ $location = $this->locationService->loadLocation($autoTranslationActions->getSubtreeId());
+ $message = $this->translator->trans('auto_translation.add.failed', [
+ '%subtree_name%' => $location->contentInfo->name
+ ]);
+ $event->getForm()->addError(new FormError($message));
+ $this->notificationHandler->error($message);
+ } catch (NotFoundException|UnauthorizedException $e) {
+ $event->getForm()->addError(new FormError($e->getMessage()));
+ $this->notificationHandler->error($e->getMessage());
+ }
+ }
+ })
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults(
+ [
+ 'data_class' => AutoTranslationActions::class,
+ 'attr' => ['__template' => self::TEMPLATE]
+ ]
+ );
+ }
+}
diff --git a/bundle/Form/Extension/TranslationAddType.php b/bundle/Form/Extension/TranslationAddType.php
index 481b8a5..0f2224c 100644
--- a/bundle/Form/Extension/TranslationAddType.php
+++ b/bundle/Form/Extension/TranslationAddType.php
@@ -8,6 +8,7 @@
namespace EzSystems\EzPlatformAutomatedTranslationBundle\Form\Extension;
+use Ibexa\Contracts\Core\Repository\Values\Content\Language;
use Ibexa\Core\MVC\Symfony\Locale\LocaleConverterInterface;
use Ibexa\AdminUi\Form\Type\Content\Translation\TranslationAddType as BaseTranslationAddType;
use EzSystems\EzPlatformAutomatedTranslation\Client\ClientInterface;
@@ -105,7 +106,7 @@ public function buildView(FormView $view, FormInterface $form, array $options)
$fillMap = function ($key, &$map) use ($form) {
$languages = $form->get($key);
$choices = $languages->getConfig()->getAttribute('choice_list')->getChoices();
- /** @var \eZ\Publish\API\Repository\Values\Content\Language $language */
+ /** @var Language $language */
foreach ($choices as $language) {
foreach ($this->clientProvider->getClients() as $client) {
$posix = $this->localeConverter->convertToPOSIX($language->languageCode);
diff --git a/bundle/Form/SubtreeLocationType.php b/bundle/Form/SubtreeLocationType.php
new file mode 100644
index 0000000..6f9b8ef
--- /dev/null
+++ b/bundle/Form/SubtreeLocationType.php
@@ -0,0 +1,64 @@
+add(
+ 'location',
+ IntegerType::class,
+ [
+ 'required' => true,
+ 'attr' => ['hidden' => true],
+ 'label' => false,
+ 'empty_data' => [],
+ ]
+ )
+ ->addModelTransformer($this->getDataTransformer())
+ ;
+ }
+
+ private function getDataTransformer(): DataTransformerInterface
+ {
+ return new CallbackTransformer(
+ function ($value) {
+ if (null === $value || 0 === $value) {
+ return $value;
+ }
+
+ return ['location' => !empty($value) ? $value : null];
+ },
+ function ($value) {
+ if (\is_array($value) && array_key_exists('location', $value)) {
+ return $value['location'] ?? null;
+ }
+
+ return $value;
+ }
+ );
+ }
+
+ public function getName(): string
+ {
+ return 'subtree_location';
+ }
+}
diff --git a/bundle/Handler/AutoTranslationActionsHandler.php b/bundle/Handler/AutoTranslationActionsHandler.php
new file mode 100644
index 0000000..091eb72
--- /dev/null
+++ b/bundle/Handler/AutoTranslationActionsHandler.php
@@ -0,0 +1,183 @@
+connection = $connection;
+ $this->criteriaConverter = $criteriaConverter;
+ $this->permissionCriterionResolver = $permissionCriterionResolver;
+ }
+
+ public function getAllQuery(array $sort = []): QueryBuilder
+ {
+ $selectQuery = $this->getSelectQuery();
+
+ $selectQuery
+ ->addSelect('c.name as content_name')
+ ->addSelect('us.name as user_name')
+ ->addSelect('c.id as content_id')
+ ->innerJoin(
+ 'at',
+ LocationGateway::CONTENT_TREE_TABLE,
+ 't',
+ 't.node_id = at.subtree_id'
+ )
+ ->innerJoin(
+ 't',
+ ContentGateway::CONTENT_ITEM_TABLE,
+ 'c',
+ 'c.id = t.contentobject_id'
+ )
+ ->innerJoin(
+ 'at',
+ ContentGateway::CONTENT_ITEM_TABLE,
+ 'us',
+ 'us.id = at.user_id'
+ )
+ ->andWhere(
+ $selectQuery->expr()->eq('c.status', ':content_status')
+ )
+ ->setParameter(':content_status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER)
+ ;
+ if (isset($sort['field'])) {
+ $selectQuery->orderBy($sort['field'], ($sort['direction'] ?? 0) == 0 ? 'DESC' : 'ASC');
+ } else {
+ $selectQuery->orderBy('created_at', 'DESC');
+ }
+ // Check read access to whole source subtree
+ $permissionCriterion = $this->permissionCriterionResolver->getPermissionsCriterion();
+ if ($permissionCriterion !== true && $permissionCriterion !== false) {
+ $query = new Query();
+ $query->filter = new LogicalAnd(
+ [
+ new Criterion\MatchAll(),
+ $permissionCriterion,
+ ]
+ );
+ $selectQuery->andWhere($this->criteriaConverter->convertCriteria($selectQuery, $permissionCriterion, []));
+ }
+
+ return $selectQuery;
+ }
+
+
+ /**
+ * @return QueryBuilder
+ */
+ private function getSelectQuery(): QueryBuilder
+ {
+ $selectQuery = $this->connection->createQueryBuilder();
+ $selectQuery
+ ->select('at.*')
+ ->from(self::TABLE_NAME, 'at')
+ ;
+
+ return $selectQuery;
+ }
+
+ /**
+ * @throws Exception
+ * @throws \Doctrine\DBAL\Driver\Exception
+ */
+ public function getFirstPendingAction()
+ {
+ $queryBuilder = $this->getAllQuery(['field' => 'created_at', 'direction' => 1]);
+ $queryBuilder
+ ->andWhere($queryBuilder->expr()->in('at.status', ':action_status'))
+ ->setParameter(
+ ':action_status',
+ [AutoTranslationActions::STATUS_PENDING, AutoTranslationActions::STATUS_IN_PROGRESS],
+ Connection::PARAM_STR_ARRAY
+ );
+ $queryBuilder->setMaxResults(1);
+
+ return $queryBuilder->execute()->fetchAllAssociative()[0] ?? null;
+ }
+
+ public function countAll(QueryBuilder $queryBuilder): int
+ {
+ $queryBuilder->select('COUNT(at.id)');
+ $queryBuilder->orderBy('at.id');
+ $queryBuilder->setMaxResults(1);
+
+ return (int) $queryBuilder->execute()->fetchOne();
+ }
+
+ public function getContentsWithRelationsInSubtree(string $locationPath, int $offset = 0 , int $limit = 10): array
+ {
+ $sql = $this->getSqlContentsWithRelationsInSubtree();
+ $query = $this->connection->prepare(
+ "SELECT * FROM ($sql) x LIMIT $offset,$limit");
+ $query->bindValue( ':status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER);
+ $query->bindValue(':path', $locationPath . '%', ParameterType::STRING);
+ $result = $query->executeQuery();
+
+ return $result->fetchAllAssociative();
+ }
+ public function countContentWithRelationsInSubtree(string $locationPath): int
+ {
+ $sql = $this->getSqlContentsWithRelationsInSubtree();
+ $query = $this->connection->prepare(
+ "SELECT COUNT(*) FROM ($sql) x");
+ $query->bindValue( ':status', ContentInfo::STATUS_PUBLISHED, ParameterType::INTEGER);
+ $query->bindValue(':path', $locationPath . '%', ParameterType::STRING);
+ $result = $query->executeQuery();
+
+ return $result->fetchOne();
+ }
+ public function getSqlContentsWithRelationsInSubtree(): string
+ {
+ $query = $this->connection->createQueryBuilder()
+ ->from(ContentGateway::CONTENT_ITEM_TABLE, 'c')
+ ->innerJoin('c', LocationGateway::CONTENT_TREE_TABLE, 't', 't.contentobject_id = c.id')
+ ->where('c.status = :status')
+ ->andWhere('t.path_string LIKE :path');
+
+ $contentsSqlQuery = $query
+ ->select('DISTINCT c.id as contentId')
+ ->getSQL();
+
+ $relationContentsSqlQuery = $query
+ ->select('DISTINCT c_rel.to_contentobject_id as contentId')
+ ->innerJoin('c', ContentGateway::CONTENT_RELATION_TABLE, 'c_rel', 'c_rel.from_contentobject_id = c.id AND c_rel.from_contentobject_version = c.current_version')
+ ->getSQL();
+
+ return "$contentsSqlQuery UNION ALL $relationContentsSqlQuery";
+ }
+
+}
+
diff --git a/bundle/Repository/AutoTranslationActionsRepository.php b/bundle/Repository/AutoTranslationActionsRepository.php
new file mode 100644
index 0000000..ba743fc
--- /dev/null
+++ b/bundle/Repository/AutoTranslationActionsRepository.php
@@ -0,0 +1,9 @@
+ {
- Encore.addEntry('ezplatform-automated-translation-js', [path.resolve(__dirname, '../public/admin/js/ezplatformautomatedtranslation.js')]);
-};
\ No newline at end of file
diff --git a/bundle/Resources/encore/ibexa.config.js b/bundle/Resources/encore/ibexa.config.js
new file mode 100644
index 0000000..8034383
--- /dev/null
+++ b/bundle/Resources/encore/ibexa.config.js
@@ -0,0 +1,16 @@
+const path = require('path');
+
+module.exports = (Encore) => {
+ Encore.addEntry('ezplatform-automated-translation-js', [
+ path.resolve('./public/bundles/ibexaadminui/js/scripts/admin.content.edit.js'),
+ path.resolve('./public/bundles/ibexaadminui/js/scripts/fieldType/base/base-field.js'),
+ path.resolve(__dirname, '../public/admin/js/validator/auto-translation-ezselection.js'),
+ path.resolve(__dirname, '../public/admin/js/validator/auto-translation-ezobjectrelationlist.js'),
+ path.resolve(__dirname, '../public/admin/js/ezplatformautomatedtranslation.js'),
+ path.resolve(__dirname, '../public/admin/js/auto-translation.js'),
+ path.resolve(__dirname, '../public/admin/js/show.history.js'),
+ ]);
+ Encore.addEntry('ezplatform-automated-translation-css', [
+ path.resolve(__dirname, '../public/admin/scss/auto-translation.scss'),
+ ]);
+};
\ No newline at end of file
diff --git a/bundle/Resources/public/admin/js/auto-translation.js b/bundle/Resources/public/admin/js/auto-translation.js
new file mode 100644
index 0000000..856ac35
--- /dev/null
+++ b/bundle/Resources/public/admin/js/auto-translation.js
@@ -0,0 +1,92 @@
+import SelectedLocationsComponent from './components/selected.locations.component.js';
+const { ibexa } = window;
+
+(function (global, doc) {
+ const udwContainer = doc.querySelector('#react-udw');
+ const formSearch = doc.querySelector('form[name="auto_translation_actions_search"]');
+ const sortableColumns = doc.querySelectorAll('.ibexa-table__sort-column');
+ const sortedActiveField = doc.querySelector('#auto_translation_actions_search_sort_field').value;
+ const sortedActiveDirection = doc.querySelector('#auto_translation_actions_search_sort_direction').value;
+ const sortField = doc.querySelector('#auto_translation_actions_search_sort_field');
+ const sortDirection = doc.querySelector('#auto_translation_actions_search_sort_direction');
+ const CLASS_SORTED_ASC = 'ibexa-table__sort-column--asc';
+ const CLASS_SORTED_DESC = 'ibexa-table__sort-column--desc';
+
+ const closeUDW = () => ReactDOM.unmountComponentAtNode(udwContainer);
+ const selectLocationBut = doc.querySelector('.js-auto_translation-select-location-id');
+ function notify(message, type = 'info') {
+ if (!message) return;
+ const eventInfo = new CustomEvent('ibexa-notify', {
+ detail: {
+ label: type,
+ message: message
+ }
+ });
+ document.body.dispatchEvent(eventInfo);
+ }
+ if (selectLocationBut) {
+ let udwRoot = null;
+
+ selectLocationBut.addEventListener('click', function (e) {
+ e.preventDefault();
+ const clickedButton = e.target;
+ const config = JSON.parse(e.currentTarget.dataset.udwConfig);
+ const selectedLocationList = doc.querySelector(clickedButton.dataset.selectedLocationListSelector);
+ ReactDOM.render(React.createElement(ibexa.modules.UniversalDiscovery, {
+ onConfirm: (data) => {
+ ReactDOM.render(React.createElement(SelectedLocationsComponent, {
+ items: data,
+ onDelete: (locations) => {
+ doc.querySelector(clickedButton.dataset.locationInputSelector).value = locations.map(location => location.id).join();
+ }
+ }), selectedLocationList);
+
+ doc.querySelector(clickedButton.dataset.locationInputSelector).value = data.map(location => location.id).join();
+ closeUDW();
+ const formError = clickedButton.closest('.auto-translation-field--ezobjectrelationlist')
+ .querySelector('.ibexa-form-error');
+ if(formError) {
+ formError.innerHTML = '';
+ }
+ },
+ onCancel: () => {
+ closeUDW();
+ },
+ ...config
+ }), udwContainer);
+ });
+ }
+ const sortItems = (event) => {
+ const { target } = event;
+ const { field, direction } = target.dataset;
+
+ sortField.value = field;
+ target.dataset.direction = direction === 'ASC' ? 'DESC' : 'ASC';
+ sortDirection.setAttribute('value', direction === 'DESC' ? 1 : 0);
+ formSearch.submit();
+ };
+
+ const setSortedClass = () => {
+ doc.querySelectorAll('.ibexa-table__sort-column').forEach((node) => {
+ node.classList.remove(CLASS_SORTED_ASC, CLASS_SORTED_DESC);
+ });
+
+ if (sortedActiveField) {
+ const sortedFieldNode = doc.querySelector(`.ibexa-table__sort-column--${sortedActiveField}`);
+
+ if (!sortedFieldNode) {
+ return;
+ }
+
+ if (parseInt(sortedActiveDirection, 10) === 1) {
+ sortedFieldNode.classList.add(CLASS_SORTED_ASC);
+ } else {
+ sortedFieldNode.classList.add(CLASS_SORTED_DESC);
+ }
+ }
+ };
+
+ setSortedClass();
+ sortableColumns.forEach((column) => column.addEventListener('click', sortItems, false));
+
+})(window, document);
diff --git a/bundle/Resources/public/admin/js/components/selected.location.component.js b/bundle/Resources/public/admin/js/components/selected.location.component.js
new file mode 100644
index 0000000..7ce13c2
--- /dev/null
+++ b/bundle/Resources/public/admin/js/components/selected.location.component.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon';
+
+const SelectedLocationComponent = (props) => {
+ const { location, onDelete } = props;
+ const handleClick = function (event) {
+ onDelete(event.currentTarget.dataset.locationId);
+ };
+ return (
+
+ + {{ 'Num' }} + + | ++ + {{ 'auto_translation.list.user_name'|trans }} + + | ++ + {{ 'auto_translation.list.content_name'|trans }} + + | ++ + {{ 'auto_translation.list.target_language'|trans }} + + | ++ + {{ 'auto_translation.list.overwrite'|trans }} + + | + + {{ 'auto_translation.list.created_at'|trans }} + + | ++ + {{ 'auto_translation.list.status'|trans }} + + | ++ + {{ 'Log'|trans }} + + | +
---|---|---|---|---|---|---|---|
+ {{ item.id }} + | ++ {{ item.user_name }} + | ++ {{ item.content_name }} + | +{{ ibexa_admin_ui_config.languages.mappings[item.target_language].name }} | +{{ 'auto_translation.list.overwrite.value'|trans({'%count%': item.overwrite})|raw }} | +{{ item.created_at|date("d/m/Y H:i") }} | +
+
+ {{ ('auto_translation.list.status.' ~ item.status)|trans }}
+
+ {% if item.finished_at and item.status == 'finished' or item.status == 'failed' %}
+ ({{ item.finished_at|date("d/m/Y H:i") }}) + {% endif %} + |
+
+ {% if item.finished_at and item.status == 'finished' or item.status == 'failed' %}
+
+
+
+ {% endif %}
+
+
+
+ |
+