local_translation-8.x-1.x-dev/modules/local_translation_content/src/Controller/LocalTranslationContentController.php
modules/local_translation_content/src/Controller/LocalTranslationContentController.php
<?php
namespace Drupal\local_translation_content\Controller;
use Drupal\content_translation\ContentTranslationManager;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\content_translation\Controller\ContentTranslationController;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\local_translation\Services\LocalTranslationUserSkills;
use Drupal\local_translation_content\Access\LocalTranslationContentManageAccess;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for entity translation controllers.
*/
class LocalTranslationContentController extends ContentTranslationController {
/**
* User skills service.
*
* @var \Drupal\local_translation\Services\LocalTranslationUserSkills
*/
protected $userSkills;
/**
* Local translation access manager.
*
* @var \Drupal\local_translation_content\Access\LocalTranslationContentManageAccess
*/
protected $accessManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('content_translation.manager'),
$container->get('content_translation.manage_access'),
$container->get('local_translation.user_skills')
);
}
/**
* LocalTranslationContentController constructor.
*
* @param \Drupal\content_translation\ContentTranslationManagerInterface $manager
* Content translation manager.
* @param \Drupal\local_translation_content\Access\LocalTranslationContentManageAccess $access_manager
* Local translation access manager.
* @param \Drupal\local_translation\Services\LocalTranslationUserSkills $user_skills
* User skills service.
*/
public function __construct(
ContentTranslationManagerInterface $manager,
LocalTranslationContentManageAccess $access_manager,
LocalTranslationUserSkills $user_skills
) {
parent::__construct($manager);
$this->userSkills = $user_skills;
$this->accessManager = $access_manager;
}
/**
* Builds the translations overview page.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param string $entity_type_id
* (optional) The entity type ID.
* @param bool $filter
* The filter option to filter content translation links or no.
*
* @return array
* Array of page elements to render.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityMalformedException
*/
public function overview(RouteMatchInterface $route_match, $entity_type_id = NULL, $filter = TRUE) {
$build = $this->parentBuildOverridden($route_match, $entity_type_id);
if (!$filter || empty($this->config('local_translation.settings')->get('enable_filter_translation_tab_to_skills'))) {
return $build;
}
$user_languages = $this->userSkills->getSkills();
if (FALSE === $user_languages) {
return $build;
}
if (empty($user_languages)) {
$this->userSkills->showEmptyMessage();
}
$rows =& $build['content_translation_overview']['#rows'];
$user_langs_rows = $other_langs_rows = [];
$extracted = $this->extractLanguagesWithGroups($rows, $user_languages);
if (isset($extracted[0]) && !empty($extracted[0])) {
$user_langs_rows = $extracted[0];
}
if (isset($extracted[1]) && !empty($extracted[1])) {
$other_langs_rows = $extracted[1];
}
if (!empty($user_langs_rows)) {
foreach ($user_langs_rows as $key => $row) {
$user_langs_rows[$key] = $rows[$row];
}
// Post processing translation operations links
// for user registered translations.
$this->postProcessTranslationsOperations(
$user_langs_rows,
$route_match->getParameter($entity_type_id)
);
}
$rows = $user_langs_rows;
if (!empty($other_langs_rows)) {
$entity_type_id = $route_match->getParameter('entity_type_id');
$build['more_link'] = [
'#title' => $this->t('Show all languages'),
'#type' => 'link',
'#attributes' => [
'class' => [
'use-ajax',
'button', 'button--small',
'more-link',
'more-link-translations',
],
'id' => 'show-more-translations-link',
],
'#url' => Url::fromRoute(
$route_match->getRouteName() . '.more',
[$entity_type_id => $route_match->getParameter($entity_type_id)->id(), 'method' => 'ajax']
),
];
$build['content_translation_overview']['#attributes']['id'] = 'content-translations-list';
$build['#attached']['library'][] = 'core/drupal.ajax';
}
return $build;
}
/**
* Process source language argument.
*
* @param string $source
* Source language ID.
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* Entity object.
*
* @return string
* Processed language ID.
*/
protected function processSourceLanguage($source, ContentEntityInterface $entity) {
if (empty($this->config('local_translation.settings')
->get('enable_auto_preset_source_language_by_skills'))) {
return $source;
}
foreach ($this->userSkills->getSourceSkills() as $langcode) {
if (!$entity->hasTranslation($langcode)) {
continue;
}
return $langcode;
}
return $source;
}
/**
* Overridden version of parent::overview().
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* Route match.
* @param string|null $entity_type_id
* Entity type ID.
*
* @return array
* Build form array.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function parentBuildOverridden(RouteMatchInterface $route_match, $entity_type_id = NULL) {
$row_title = $source_name = $this->t('n/a');
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $route_match->getParameter($entity_type_id);
$account = $this->currentUser();
$handler = $this->entityManager()->getHandler($entity_type_id, 'translation');
$manager = $this->manager;
$entity_type = $entity->getEntityType();
$use_latest_revisions = $entity_type->isRevisionable() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle());
// Start collecting the cacheability metadata, starting with the entity and
// later merge in the access result cacheability metadata.
$cacheability = CacheableMetadata::createFromObject($entity);
$languages = $this->languageManager()->getLanguages();
$original = $entity->getUntranslated()->language()->getId();
$translations = $entity->getTranslationLanguages();
$field_ui = $this->moduleHandler()->moduleExists('field_ui') && $account->hasPermission('administer ' . $entity_type_id . ' fields');
$rows = [];
$show_source_column = FALSE;
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($entity_type_id);
$default_revision = $storage->load($entity->id());
if ($this->languageManager()->isMultilingual()) {
// Determine whether the current entity is translatable.
$translatable = FALSE;
foreach ($this->entityManager()->getFieldDefinitions($entity_type_id, $entity->bundle()) as $instance) {
if ($instance->isTranslatable()) {
$translatable = TRUE;
break;
}
}
// Show source-language column if there are non-original source langcodes.
$additional_source_langcodes = array_filter(array_keys($translations), function ($langcode) use ($entity, $original, $manager) {
$source = $manager->getTranslationMetadata($entity->getTranslation($langcode))->getSource();
return $source != $original && $source != LanguageInterface::LANGCODE_NOT_SPECIFIED;
});
$show_source_column = !empty($additional_source_langcodes);
foreach ($languages as $language) {
$language_name = $language->getName();
$langcode = $language->getId();
// If the entity type is revisionable, we may have pending revisions
// with translations not available yet in the default revision. Thus we
// need to load the latest translation-affecting revision for each
// language to be sure we are listing all available translations.
if ($use_latest_revisions) {
$entity = $default_revision;
$latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
if ($latest_revision_id) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */
$latest_revision = $storage->loadRevision($latest_revision_id);
// Make sure we do not list removed translations, i.e. translations
// that have been part of a default revision but no longer are.
if (!$latest_revision->wasDefaultRevision() || $default_revision->hasTranslation($langcode)) {
$entity = $latest_revision;
}
}
$translations = $entity->getTranslationLanguages();
}
$add_url = new Url(
"entity.$entity_type_id.content_translation_add",
[
// Additionally process the original language
// in order to try it be one of the translation skills.
'source' => $this->processSourceLanguage($original, $entity),
'target' => $language->getId(),
$entity_type_id => $entity->id(),
],
[
'language' => $language,
]
);
$edit_url = new Url(
"entity.$entity_type_id.content_translation_edit",
[
'language' => $language->getId(),
$entity_type_id => $entity->id(),
],
[
'language' => $language,
]
);
$delete_url = new Url(
"entity.$entity_type_id.content_translation_delete",
[
'language' => $language->getId(),
$entity_type_id => $entity->id(),
],
[
'language' => $language,
]
);
$operations = [
'data' => [
'#type' => 'operations',
'#links' => [],
],
];
$links = &$operations['data']['#links'];
if (array_key_exists($langcode, $translations)) {
// Existing translation in the translation set: display status.
$translation = $entity->getTranslation($langcode);
$metadata = $manager->getTranslationMetadata($translation);
$source = $metadata->getSource() ?: LanguageInterface::LANGCODE_NOT_SPECIFIED;
$is_original = $langcode == $original;
$label = $entity->getTranslation($langcode)->label();
$link = isset($links->links[$langcode]['url']) ? $links->links[$langcode] : ['url' => $entity->urlInfo()];
if (!empty($link['url'])) {
$link['url']->setOption('language', $language);
$row_title = $this->l($label, $link['url']);
}
if (empty($link['url'])) {
$row_title = $is_original ? $label : $this->t('n/a');
}
// If the user is allowed to edit the entity we point the edit link to
// the entity form, otherwise if we are not dealing with the original
// language we point the link to the translation form.
$update_access = $entity->access('update', NULL, TRUE);
$translation_access = $handler->getTranslationAccess($entity, 'update');
$is_allowed = $this->accessManager->checkAccess($entity, $language, 'update')->isAllowed();
$cacheability = $cacheability
->merge(CacheableMetadata::createFromObject($update_access))
->merge(CacheableMetadata::createFromObject($translation_access));
if ($is_allowed && $update_access->isAllowed() && $entity_type->hasLinkTemplate('edit-form')) {
$links['edit']['url'] = $entity->urlInfo('edit-form');
$links['edit']['language'] = $language;
}
elseif (!$is_original && $translation_access->isAllowed()) {
$links['edit']['url'] = $edit_url;
}
elseif ($is_original && $is_allowed) {
$links['edit']['url'] = $edit_url;
}
if (isset($links['edit'])) {
$links['edit']['title'] = $this->t('Edit');
}
$status = [
'data' => [
'#type' => 'inline_template',
'#template' => '<span class="status">{% if status %}{{ "Published"|t }}{% else %}{{ "Not published"|t }}{% endif %}</span>{% if outdated %} <span class="marker">{{ "outdated"|t }}</span>{% endif %}',
'#context' => [
'status' => $metadata->isPublished(),
'outdated' => $metadata->isOutdated(),
],
],
];
if ($is_original) {
$language_name = $this->t('<strong>@language_name (Original language)</strong>', ['@language_name' => $language_name]);
$source_name = $this->t('n/a');
}
else {
/** @var \Drupal\Core\Access\AccessResultInterface $delete_route_access */
$delete_route_access = $this->accessManager->checkAccess($translation, $language, 'delete');
$cacheability->addCacheableDependency($delete_route_access);
if ($delete_route_access->isAllowed()) {
$source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a');
$delete_access = $entity->access('delete', NULL, TRUE);
$translation_access = $handler->getTranslationAccess($entity, 'delete');
$cacheability
->addCacheableDependency($delete_access)
->addCacheableDependency($translation_access);
if ($delete_access->isAllowed() && $entity_type->hasLinkTemplate('delete-form')) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => $entity->urlInfo('delete-form'),
'language' => $language,
];
}
elseif ($translation_access->isAllowed()) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => $delete_url,
];
}
}
elseif (!$entity->hasTranslation($langcode)
|| (method_exists($entity, 'isPublished') && !$entity->isPublished())
) {
$this->messenger()->addWarning($this->t('The "Delete translation" action is only available for published translations.'), FALSE);
}
}
}
else {
// No such translation in the set yet: help user to create it.
$row_title = $source_name = $this->t('n/a');
$source = $entity->language()->getId();
$create_translation_access = $handler->getTranslationAccess($entity, 'create');
$cacheability = $cacheability
->merge(CacheableMetadata::createFromObject($create_translation_access));
if ($source != $langcode && $create_translation_access->isAllowed()) {
if ($translatable) {
$links['add'] = [
'title' => $this->t('Add'),
'url' => $add_url,
];
}
elseif ($field_ui) {
$url = new Url('language.content_settings_page');
// Link directly to the fields tab to make it easier to find the
// setting to enable translation on fields.
$links['nofields'] = [
'title' => $this->t('No translatable fields'),
'url' => $url,
];
}
}
$status = $this->t('Not translated');
}
if ($show_source_column) {
$rows[] = [
$language_name,
$row_title,
$source_name,
$status,
$operations,
];
}
else {
$rows[] = [$language_name, $row_title, $status, $operations];
}
}
}
if ($show_source_column) {
$header = [
$this->t('Language'),
$this->t('Translation'),
$this->t('Source language'),
$this->t('Status'),
$this->t('Operations'),
];
}
else {
$header = [
$this->t('Language'),
$this->t('Translation'),
$this->t('Status'),
$this->t('Operations'),
];
}
$build['#title'] = $this->t('Translations of %label', ['%label' => $entity->label()]);
// Add metadata to the build render array to let other modules know about
// which entity this is.
$build['#entity'] = $entity;
$cacheability
->addCacheTags($entity->getCacheTags())
->applyTo($build);
$build['content_translation_overview'] = [
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
];
return $build;
}
/**
* Get more languages.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* Route match interface.
* @param string|null $entity_type_id
* Entity type ID.
* @param string $method
* Method name. Values allowed - "noajax" and "ajax". Defaults to "ajax".
*
* @return array|\Drupal\Core\Ajax\AjaxResponse
* Array of languages or AJAX response.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityMalformedException
*/
public function getMoreLanguages(RouteMatchInterface $route_match, $entity_type_id = NULL, $method = 'ajax') {
$build = self::overview($route_match, $entity_type_id, FALSE);
$rows =& $build['content_translation_overview']['#rows'];
$user_languages = $this->userSkills->getSkills();
$user_langs_rows = $other_langs_rows = [];
$extracted = $this->extractLanguagesWithGroups($rows, $user_languages);
if (isset($extracted[0]) && !empty($extracted[0])) {
$user_langs_rows = $extracted[0];
}
if (isset($extracted[1]) && !empty($extracted[1])) {
$other_langs_rows = $extracted[1];
}
$other_langs_rows = array_intersect_key($rows, array_flip($other_langs_rows));
if ($method == 'noajax') {
$rows = $other_langs_rows;
return $build;
}
elseif ($method == 'ajax') {
$response = new AjaxResponse();
foreach ($user_langs_rows as $key => $row) {
$user_langs_rows[$key] = $rows[$row];
}
// Post processing translation operations links
// for user registered translations.
$this->postProcessTranslationsOperations(
$user_langs_rows,
$route_match->getParameter($entity_type_id)
);
$rows = array_merge($user_langs_rows, $other_langs_rows);
$replace = new ReplaceCommand('#content-translations-list', $build['content_translation_overview']);
$remove = new RemoveCommand('#show-more-translations-link');
$response->addCommand($replace);
$response->addCommand($remove);
return $response;
}
return [];
}
/**
* Extract language from row.
*
* @param array &$row
* Row array.
*
* @return mixed
* Extracted language from row.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
private function extractLanguageFromRow(array &$row) {
$label = reset($row);
self::extractDefaultLanguageName($label);
return !is_string($label)
? $this->languageManager->getDefaultLanguage()
: $this->getLanguageByLabel($label);
}
/**
* Extract languages with groups.
*
* @param array &$rows
* Rows array.
* @param array $user_languages
* User languages array.
*
* @return array
* Languages array.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function extractLanguagesWithGroups(array &$rows, array $user_languages) {
$groups = []; $original_key = NULL;
foreach ($rows as $key => $row) {
$language = $this->extractLanguageFromRow($row);
$is_original = stripos((string) reset($row), 'original') !== FALSE;
$delta = $language instanceof LanguageInterface
&& (in_array($language->getId(), $user_languages) || ($is_original && !empty($this->config('local_translation.settings')->get('always_display_original_language_translation_tab'))))
? 0 : 1;
if ($is_original) {
$original_key = $key;
}
$groups[$delta][] = $key;
}
// Move original language to the top of the array if visable.
if (!is_null($original_key) && !empty($groups[0]) && in_array($original_key, $groups[0])) {
$original_key = array_search($original_key, $groups[0]);
$original = $groups[0][$original_key];
unset($groups[0][$original_key]);
array_unshift($groups[0], $original);
}
return $groups;
}
/**
* Get language config entity.
*
* @param string|null $label
* Language label.
*
* @return \Drupal\Core\Language\LanguageInterface|mixed|null
* Language object if exists.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getLanguageByLabel($label = NULL) {
if (empty($label)) {
return NULL;
}
$languages = $this->entityTypeManager
->getStorage('configurable_language')
->loadByProperties(['label' => $label]);
return !empty($languages) ? reset($languages) : NULL;
}
/**
* Additional post processing function.
*
* Post processing translation operations links
* for user registered translations.
*
* @param array $user_langs_rows
* Rows array.
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* Processed entity.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityMalformedException
*/
protected function postProcessTranslationsOperations(array &$user_langs_rows, ContentEntityInterface $entity) {
foreach ($user_langs_rows as &$langs_row) {
if (!empty($langs_row) && is_array($langs_row)) {
$label = reset($langs_row);
self::extractDefaultLanguageName($label);
if (!empty($label) && is_string($label)) {
$key = self::getLastArrayKey($langs_row);
$operations =& $langs_row[$key]['data']['#links'];
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $language */
$language = static::getLanguageByLabel($label);
$language_object = $this->languageManager
->getLanguage($language->id());
if ($entity->hasTranslation($language->id())) {
if ($this->isOperationAllowed($entity, $language_object, 'update')) {
$operations['edit'] = [
'url' => $entity->toUrl('edit-form'),
'language' => $language,
'title' => $this->t('Edit'),
];
}
if ($this->isOperationAllowed($entity, $language_object, 'delete')
&& $entity->getEntityType()->hasLinkTemplate('delete-form')) {
$operations['delete'] = [
'url' => $entity->toUrl('delete-form'),
'language' => $language,
'title' => $this->t('Delete'),
];
}
}
elseif ($this->isOperationAllowed($entity, $language_object, 'create')) {
$operations['add'] = [
'url' => $this->buildTranslationCreateUrl($entity, $language),
'language' => $language,
'title' => $this->t('Add'),
];
}
else {
unset($operations['add']);
}
}
}
}
}
/**
* Small helper method for extracting the default language label.
*
* @param string|\Drupal\Core\StringTranslation\TranslatableMarkup &$name
* Language name/label.
*/
private static function extractDefaultLanguageName(&$name) {
if ($name instanceof TranslatableMarkup) {
$name = $name->getArguments()['@language_name'];
}
}
/**
* Simple wrapper for checking entity operation access.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* Content entity.
* @param \Drupal\Core\Language\LanguageInterface $language
* Language object.
* @param string $op
* Operation name. Defaults to "delete".
*
* @return bool
* Access checking result.
*/
protected function isOperationAllowed(ContentEntityInterface $entity, LanguageInterface $language = NULL, $op = 'delete') {
if (!$language instanceof LanguageInterface) {
$language = $this->languageManager
->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
}
return $this->accessManager
->checkAccess($entity, $language, $op)
->isAllowed();
}
/**
* Helper method to build "Add" url for translation.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* Entity object.
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $language
* Translation language entity.
*
* @return \Drupal\Core\Url
* URL object.
*/
private function buildTranslationCreateUrl(ContentEntityInterface $entity, ConfigEntityInterface $language) {
$entity_type_id = $entity->getEntityTypeId();
$route_name = "entity.$entity_type_id.content_translation_add";
return Url::fromRoute($route_name, [
'source' => $this->processSourceLanguage($entity->getUntranslated()->language()->getId(), $entity),
'target' => $language->id(),
$entity_type_id => $entity->id(),
]);
}
/**
* Get last array key.
*
* @param array $array
* Array to be processed.
*
* @return mixed
* Last array key.
*/
private static function getLastArrayKey(array $array) {
$keys = array_keys($array);
return end($keys);
}
}
