tmgmt_xtm-8.x-5.x-dev/src/Plugin/tmgmt/Translator/Cart.php
src/Plugin/tmgmt/Translator/Cart.php
<?php
namespace Drupal\tmgmt_xtm\Plugin\tmgmt\Translator;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\tmgmt\Entity\Job;
use Drupal\tmgmt\Entity\JobItem;
use Drupal\tmgmt\Entity\Translator;
use Masterminds\HTML5\Exception;
/**
* Provides operations for managing translation jobs.
*
* @package Drupal\tmgmt_xtm\Plugin\tmgmt\Translator
*/
class Cart {
/**
* Requests an operation for multiple translation jobs.
*
* @param array $job
* The job to be processed.
* @param array $jobItems
* The items associated with the job.
* @param array $context
* The context for the operation.
*/
public function requestOperationMultiple(array $job, array $jobItems, array &$context) {
$translator = $this->getTranslator($job['translator']);
if (is_null($translator)) {
\Drupal::messenger()->addStatus(t('No translator available.'));
return;
}
$helper = new Helper();
$jobItemsBySourceLanguage = $this->groupSelectedItemsBySourceLanguage($jobItems, $job);
$targetLanguages = $job['target_languages'];
$jobs = $jobResults = $removeJobItemIds = [];
foreach ($targetLanguages as $targetLanguage) {
foreach ($jobItemsBySourceLanguage as $sourceLanguage => $job_items) {
if ($sourceLanguage == $targetLanguage) {
continue;
}
$jobModel = tmgmt_job_create(
$sourceLanguage,
$targetLanguage,
\Drupal::currentUser()->id()
);
$jobEmptyFlag = TRUE;
foreach ($job_items as $id => $jobItem) {
/** @var \Drupal\tmgmt\Entity\JobItem $jobItem */
try {
$jobModel->addItem($jobItem->getPlugin(), $jobItem->getItemType(), $jobItem->getItemId());
$jobItemId = $jobItem->id();
$removeJobItemIds[$jobItemId] = $jobItemId;
$jobResults['job_items'][$jobItemId] = $jobItemId;
$jobEmptyFlag = FALSE;
}
catch (\Exception $e) {
unset($jobItemsBySourceLanguage[$sourceLanguage][$id]);
\Drupal::messenger()->addError($e->getMessage());
}
}
if ($job['enforced_source_language']) {
$jobModel->set('source_language', $job['forced_source_language']);
}
$this->createJobs($job, $jobEmptyFlag, $jobModel, $jobs);
}
$this->removeJobItemsFromCart($removeJobItemIds);
}
if (!$jobs) {
\Drupal::messenger()->addStatus(t('From the selection you made it was not possible to create any translation job..'));
return;
}
if ($job['enforced_source_language']) {
$this->createDrupalMessageOnEnforceSourceLanguage();
$this->createSkippedCountMessage(0, $job['enforced_source_language']);
}
$connector = new MultipleTargetLanguageConnector();
$connector->xtmRequestTranslation($jobs);
$context['results'][] = $jobResults;
$context['message'] = t('Creating a project for the langauge "@name".',
['@name' => $helper->mapLanguageToXTMFormat($job['source_language'], $translator)]);
\Drupal::messenger()->addStatus(t('The Translations request has been submitted successfully.'));
}
/**
* Retrieves a translator entity by its name.
*
* This method searches through the available translators and returns the
* translator that matches the provided name.
*
* @param string $name
* The name of the translator to retrieve.
*
* @return \Drupal\tmgmt\Entity\Translator|null
* The translator entity if found, NULL if no matching translator found.
*/
private function getTranslator($name = "") {
$translator = NULL;
foreach (tmgmt_translator_load_available(NULL) as $translator) {
/** @var \Drupal\tmgmt\Entity\Translator $translator */
if ($translator->id() == $name) {
break;
}
}
return $translator;
}
/**
* Groups job items by their source language.
*
* @param array $jobItems
* An array of job items to be grouped.
* @param array $job
* An array containing job-related information, including the enforced source language.
*
* @return array
* An associative array where the keys are source languages and the values are arrays of job items.
*/
private function groupSelectedItemsBySourceLanguage(array $jobItems, array $job) {
$enforcedSourceLanguage = NULL;
if ($job['enforced_source_language']) {
$enforcedSourceLanguage = $job['forced_source_language'];
}
$skippedCount = 0;
$jobItemsBySourceLanguage = [];
foreach ($jobItems as $jobItem) {
$sourceLanguage = $enforcedSourceLanguage ?: $jobItem->getSourceLangCode();
/** @var \Drupal\tmgmt\Entity\JobItem $jobItem */
if (in_array($sourceLanguage, $jobItem->getExistingLangCodes())) {
$jobItemsBySourceLanguage[$sourceLanguage][$jobItem->id()] = $jobItem;
}
else {
$skippedCount++;
}
}
if ($skippedCount > 0) {
$languages = \Drupal::languageManager()->getLanguages();
\Drupal::messenger()->addWarning(\Drupal::translation()->formatPlural(
$skippedCount,
'One item skipped as for the language @language it was not possible to retrieve a translation.',
'@count items skipped as for the language @language it was not possible to retrieve a translations.',
['@language' => $languages[$enforcedSourceLanguage]->getName()]
));
}
return $jobItemsBySourceLanguage;
}
/**
* Creates job instances based on the provided job data and adds them to the list of jobs.
*
* @param array $job
* An array containing job details including translator, template, and project mode.
* @param bool $jobEmpty
* Indicates whether the job is empty or not.
* @param Job $jobModel
* The job model entity that will be populated and saved.
* @param array $jobs
* A reference to an array where created job instances will be added.
*
* @return MessengerInterface|void
* @throws EntityStorageException
*/
private function createJobs(array &$job, bool $jobEmpty, Job $jobModel, array &$jobs) {
if ($jobEmpty) {
return;
}
$translator = $this->getTranslator($job['translator']);
$jobModel->set('translator', $translator);
$settings = [];
if (isset($job['template'])) {
$settings[Connector::API_TEMPLATE_ID] = $job['template'];
}
if (isset($job['project_mode'])) {
$settings[Connector::API_PROJECT_MODE] = $job['project_mode'];
}
$jobModel->set('settings', $settings);
$translatable = $jobModel->canRequestTranslation();
if (!$translatable->getSuccess()) {
return \Drupal::messenger()->addWarning($translatable->getReason());
}
$jobModel->save();
$jobs[] = $jobModel;
}
/**
* Removes job items from the cart and deletes them from storage.
*
* @param array $removeJobItemIds
* An array of job item IDs to be removed and deleted.
*
* @return void
*/
private function removeJobItemsFromCart(array $removeJobItemIds) {
if ($removeJobItemIds) {
tmgmt_cart_get()->removeJobItems($removeJobItemIds);
$storage_handler = \Drupal::entityTypeManager()
->getStorage('tmgmt_job_item');
$entities = $storage_handler->loadMultiple($removeJobItemIds);
$storage_handler->delete($entities);
}
}
/**
* Creates a warning message about the enforced source language.
*
* @return void
*/
private function createDrupalMessageOnEnforceSourceLanguage() {
\Drupal::messenger()->addWarning(
t('You have enforced the job source language which most likely resulted in having a
translation of your original content as the job source text. You should review the job translation
received from the translator carefully to prevent content quality loss.')
);
}
/**
* Creates an error message for skipped items based on the count and source language.
*
* @param int $skippedCount
* The number of items skipped due to the language not being available for translation.
* @param string $enforcedSourceLanguage
* The source language code that was enforced for translation.
*
* @return void
* This method does not return a value.
*/
private function createSkippedCountMessage(int $skippedCount, string $enforcedSourceLanguage) {
if ($skippedCount) {
$languages = \Drupal::languageManager()->getLanguages();
\Drupal::messenger()->addError(\Drupal::translation()->formatPlural(
$skippedCount,
'One item skipped as for the language @language it was not possible to retrieve a translation.',
'@count items skipped as for the language @language it was not possible to retrieve a translations.',
['@language' => $languages[$enforcedSourceLanguage]->getName()]
));
}
}
/**
* Handles the operation request by creating translation jobs and updating the cart.
*
* @param array $job
* An associative array containing job-related data. Keys include:
* - 'translator': The translator ID.
* - 'target_language': The target language code for the translation.
* - 'source_language': The source language code for the translation.
* - 'enforced_source_language': The source language code enforced for the translation (if any).
* @param array $jobItems
* An array of job items to be processed. Each item is an instance of `\Drupal\tmgmt\Entity\JobItem`.
* @param array &$context
* An array used to pass data between different parts of the system. This method adds job results and messages to this array.
*
* @return void
* It updates the context and messages directly.
*/
public function requestOperation(array $job, array $jobItems, array &$context) {
$translator = $this->getTranslator($job['translator']);
if (is_null($translator)) {
\Drupal::messenger()->addStatus(t('No translator available.'));
return;
}
$helper = new Helper();
$jobItemsBySourceLanguage = $this->groupSelectedItemsBySourceLanguage($jobItems, $job);
$targetLanguage = $job['target_language'];
$jobs = $jobResults = $removeJobItemIds = [];
foreach ($jobItemsBySourceLanguage as $sourceLanguage => $job_items) {
if ($sourceLanguage == $targetLanguage) {
continue;
}
$jobModel = tmgmt_job_create($sourceLanguage, $targetLanguage, \Drupal::currentUser()->id());
$jobEmptyFlag = TRUE;
foreach ($job_items as $id => $jobItem) {
/** @var \Drupal\tmgmt\Entity\JobItem $jobItem */
try {
$jobModel->addItem($jobItem->getPlugin(), $jobItem->getItemType(), $jobItem->getItemId());
$jobItemId = $jobItem->id();
$removeJobItemIds[$jobItemId] = $jobItemId;
$jobResults['job_items'][$jobItemId] = $jobItemId;
$jobEmptyFlag = FALSE;
}
catch (Exception $e) {
unset($jobItemsBySourceLanguage[$sourceLanguage][$id]);
\Drupal::messenger()->addError($e->getMessage());
}
}
$this->createJobs($job, $jobEmptyFlag, $jobModel, $jobs);
}
$this->removeJobItemsFromCart($removeJobItemIds);
if (!$job) {
\Drupal::messenger()->addStatus(t('From the selection you made it was not possible to create any translation job.'));
return;
}
if ($job['enforced_source_language']) {
$this->createDrupalMessageOnEnforceSourceLanguage();
$this->createSkippedCountMessage(0, $job['enforced_source_language']);
}
$this->doXtmTranslation($jobs);
$context['results'][] = $jobResults;
$context['message'] = t(
'Creating a project for the language "@name".',
['@name' => $helper->mapLanguageToXTMFormat($job['source_language'], $translator)]
);
\Drupal::messenger()->addStatus(t('The Translations request has been submitted successfully.'));
}
/**
* Processes translation requests by sending them to the XTM connector.
*
* @param array $jobs
* An array of jobs to be processed. Each job should be structured according to the requirements of the XTM connector.
*
* @return void
* It performs actions to send translation requests.
*/
private function doXtmTranslation(array $jobs) {
$connector = new Connector();
foreach ($jobs as $job) {
$connector->xtmRequestTranslation($job);
}
}
/**
* Processes form submission for creating translation projects.
*
* This method handles the form submission, organizes job items by their source language,
* and prepares batch operations for creating translation projects based on the provided settings.
*
* @param \Drupal\Core\Form\FormStateInterface $formState
* The form state object containing the submitted values and other form-related data.
*
* @return void
* It sets up a batch process for creating projects.
*/
public function formSubmit(FormStateInterface $formState) {
$jobItemsBySourceLanguage = [];
$jobItemsLoad = JobItem::loadMultiple(array_filter($formState->getValue('items')));
foreach ($jobItemsLoad as $jobItem) {
/** @var \Drupal\tmgmt\Entity\JobItem $jobItem */
$jobItemsBySourceLanguage[$jobItem->getSourceLangCode()][$jobItem->id()] = $jobItem;
}
$multiLang = $formState->getValue('multi_lang');
$translator = $this->getTranslator($multiLang['xtm_translator']);
if (is_null($translator)) {
return;
}
$multipleLanguageXtmProject = $translator->getSetting('multiple_language_xtm_project');
$operations = [];
if (1 === $multipleLanguageXtmProject) {
foreach ($jobItemsBySourceLanguage as $sourceLanguage => $jobItems) {
$operations = $this->prepareMultipleOperations(
$formState,
$sourceLanguage,
array_filter($formState->getValue('target_language')),
$jobItems,
$operations
);
}
}
else {
foreach (array_filter($formState->getValue('target_language')) as $targetLanguage) {
foreach ($jobItemsBySourceLanguage as $sourceLanguage => $jobItems) {
if ($sourceLanguage == $targetLanguage) {
continue;
}
$operations = $this->prepareOperations(
$formState,
$sourceLanguage,
$targetLanguage,
$jobItems,
$operations
);
}
}
}
$batch = [
'operations' => $operations,
'finished' => 'tmgmt_xtm_cart_request_operations_finished',
'title' => t('Creating multiple projects'),
'progress_message' => t('Processed @current out of @total projects.'),
'error_message' => t('XTM translator has encountered an error.'),
];
batch_set($batch);
}
/**
* Prepares batch operations for creating jobs in multiple target language XTM projects.
*
* @param \Drupal\Core\Form\FormStateInterface $formState
* The form state object containing the submitted values and other form-related data.
* @param string $sourceLanguage
* The source language code.
* @param array $targetLanguage
* An array of target language codes.
* @param array $jobItems
* An array of job items to be included in the operations.
* @param array $operations
* The existing array of operations to be updated with new operations.
*
* @return array
* The updated array of operations including the new operations for multiple target languages.
*/
private function prepareMultipleOperations(
FormStateInterface $formState,
$sourceLanguage,
$targetLanguage,
$jobItems,
$operations,
) {
$multiLang = $formState->getValue('multi_lang');
if (!isset($multiLang['settings_fieldset']['template'])) {
$multiLang['settings_fieldset']['template'] = "";
}
$enforcedSourceLanguage = $formState->getValue('enforced_source_language');
$forcedSourceLanguage = $formState->getValue('enforced_source_language')
? $formState->getValue('source_language') : FALSE;
$operations[] = [
'tmgmt_xtm_cart_request_operation_multiple',
[
[
'source_language' => $sourceLanguage,
'target_languages' => $targetLanguage,
'user_id' => \Drupal::currentUser()->id(),
'translator' => $multiLang['xtm_translator'],
'template' => $multiLang['settings_fieldset']['template'],
'project_mode' => $multiLang['settings_fieldset']['project_mode'],
'enforced_source_language' => $enforcedSourceLanguage,
'forced_source_language' => $forcedSourceLanguage,
],
$jobItems,
],
];
return $operations;
}
/**
* Prepares batch operations for creating a job in a single target language XTM project.
*
* @param \Drupal\Core\Form\FormStateInterface $formState
* The form state object containing the submitted values and other form-related data.
* @param string $sourceLanguage
* The source language code.
* @param string $targetLanguage
* The target language code.
* @param \Drupal\tmgmt\Entity\JobItem $jobItems
* An array of job items to be included in the operation.
* @param array $operations
* The existing array of operations to be updated with a new operation.
*
* @return array
* The updated array of operations including the new operation for a single target language.
*/
private function prepareOperations(
FormStateInterface $formState,
$sourceLanguage,
$targetLanguage,
$jobItems,
$operations,
) {
$multiLang = $formState->getValue('multi_lang');
if (!isset($multiLang['settings_fieldset']['template'])) {
$multiLang['settings_fieldset']['template'] = "";
}
$forcedSourceLanguage = $formState->getValue('enforced_source_language')
? $formState->getValue('source_language') : FALSE;
$operations[] = [
'tmgmt_xtm_cart_request_operation',
[
[
'source_language' => $sourceLanguage,
'target_language' => $targetLanguage,
'user_id' => \Drupal::currentUser()->id(),
'translator' => $multiLang['xtm_translator'],
'template' => $multiLang['settings_fieldset']['template'],
'project_mode' => $multiLang['settings_fieldset']['project_mode'],
'enforced_source_language' => $formState->getValue('enforced_source_language'),
'forced_source_language' => $forcedSourceLanguage,
],
$jobItems,
],
];
return $operations;
}
/**
* Creates the cart content templates form for XTM projects.
*
* @param array $form
* The form array to be updated.
* @param \Drupal\Core\Form\FormState $formState
* The form state object.
*
* @return array
* The updated form array with cart content templates.
*/
public function getCartContentTemplates(array &$form, FormState $formState) {
$multiLang = $formState->getValue('multi_lang');
$translator = $this->getTranslator($multiLang['xtm_translator']);
if ($translator instanceof Translator) {
$form['multi_lang']['settings_fieldset']['template'] = $this->getTemplateForm($translator);
}
return $form['multi_lang']['settings_fieldset'];
}
/**
* Generates the template form for the XTM translator.
*
* @param \Drupal\tmgmt\Entity\Translator $translator
* The XTM translator entity.
*
* @return array
* The render array for the template form.
*/
private function getTemplateForm(Translator $translator) {
$connector = new Connector();
$templates = $connector->getTemplates($translator);
if (empty($templates)) {
return [
'#markup' => t('<p>There are no templates for the selected translator.</p>'),
];
}
else {
return [
'#type' => 'select',
'#title' => t('XTM project template'),
'#default_value' => NULL,
'#description' => t('Select an XTM template for each project.'),
'#options' => ['' => t('none')] + $templates,
'#name' => 'multi_lang[settings_fieldset][template]',
'#id' => 'edit-multi-lang-settings-fieldset-template',
'#validated' => TRUE,
];
}
}
/**
* Builds the form for XTM one-click multilingual projects.
*
* @param array $form
* The form array to be updated.
*
* @return void
* Returns Cart Form for XTM one-click multilingual projects.
*/
public function getForm(array &$form) {
$availableTranslators = [];
$translator = NULL;
$translators = $this->getXtmTranslators();
foreach ($translators as $translator) {
/** @var \Drupal\tmgmt\Entity\Translator $translator */
$translatorId = $translator->id();
$availableTranslators[$translatorId] = $translator->label();
}
if (empty($availableTranslators)) {
return;
}
$index = array_search('target_language', array_keys($form)) + 1;
$form = array_slice($form, 0, $index, TRUE) + ['multi_lang' => []] + array_slice(
$form,
$index,
count($form) - $index,
TRUE
);
$this->createMultiLangForm($form, $availableTranslators);
if ($translator instanceof Translator) {
$form['multi_lang']['settings_fieldset']['template'] = $this->getTemplateForm($translator);
$form['multi_lang']['settings_fieldset']['project_mode'] = $this->getProjectModeForm($translator);
}
$form['multi_lang']['request_multiple_translations'] = [
'#type' => 'submit',
'#value' => t('Request multiple translations'),
'#submit' => ['tmgmt_xtm_cart_request_translation_form_submit'],
'#validate' => ['tmgmt_xtm_cart_source_overview_validate'],
];
}
/**
* Retrieves available XTM translators.
*
* @return array
* An array of XTM translator entities.
*/
private function getXtmTranslators() {
$translators = [];
foreach (tmgmt_translator_load_available(NULL) as $translator) {
/** @var \Drupal\tmgmt\Entity\Translator $translator */
if ($translator->getPluginId() == 'xtm') {
$translators[] = $translator;
}
}
return $translators;
}
/**
* Creates the form for selecting multiple language settings.
*
* @param array $form
* The form array to be updated.
* @param array $availableTranslators
* An array of available XTM translators.
*
* @return void
*/
private function createMultiLangForm(array &$form, array $availableTranslators) {
$form['multi_lang'] = [
'#tree' => TRUE,
'#type' => 'fieldset',
'#title' => t('XTM one-click multilingual projects'),
'#description' => t('Request multiple translations with one click, skipping the Job overview settings.
Ensure to activate the option <b>Enable multilingual projects in XTM</b> in the <b>XTM Plugin settings</b> to include all target languages in the same XTM Cloud project.'),
];
$form['multi_lang']['xtm_translator'] = [
'#type' => 'select',
'#title' => t('XTM translator'),
'#default_value' => NULL,
'#description' => t('Select a translator.'),
'#options' => ['' => t('- Please choose -')] + $availableTranslators,
'#ajax' => [
'wrapper' => 'templates-fieldset-wrapper',
'callback' => 'ajax_tmgmt_xtm_cart_content_templates',
'progress' => [
'type' => 'throbber',
'message' => t('Please wait, searching for available templates...'),
],
],
];
$form['multi_lang']['settings_fieldset'] = [
'#type' => 'container',
'#prefix' => '<div id="templates-fieldset-wrapper">',
'#suffix' => '</div>',
];
}
/**
* Retrieves the project mode options form for the XTM translator.
*
* @param \Drupal\tmgmt\Entity\Translator $translator
* The XTM translator entity.
*
* @return array
* The render array for the project mode form.
*/
private function getProjectModeForm(Translator $translator) {
$helper = new Helper();
return [
'#type' => 'radios',
'#title' => t('Project mode'),
'#default_value' => !is_null($projectMode = $translator->getSetting('default_project_mode'))
? $projectMode : 0,
'#options' => $helper->getProjectModes(),
];
}
}
