class ContentTranslationController

Same name and namespace in other branches
  1. 9 core/modules/content_translation/src/Controller/ContentTranslationController.php \Drupal\content_translation\Controller\ContentTranslationController
  2. 8.9.x core/modules/content_translation/src/Controller/ContentTranslationController.php \Drupal\content_translation\Controller\ContentTranslationController
  3. 11.x core/modules/content_translation/src/Controller/ContentTranslationController.php \Drupal\content_translation\Controller\ContentTranslationController

Base class for entity translation controllers.

Hierarchy

Expanded class hierarchy of ContentTranslationController

File

core/modules/content_translation/src/Controller/ContentTranslationController.php, line 21

Namespace

Drupal\content_translation\Controller
View source
class ContentTranslationController extends ControllerBase {
  
  /**
   * The content translation manager.
   *
   * @var \Drupal\content_translation\ContentTranslationManagerInterface
   */
  protected $manager;
  
  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;
  
  /**
   * Initializes a content translation controller.
   *
   * @param \Drupal\content_translation\ContentTranslationManagerInterface $manager
   *   A content translation manager instance.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity field manager service.
   * @param \Drupal\Component\Datetime\TimeInterface|null $time
   *   The time service.
   */
  public function __construct(ContentTranslationManagerInterface $manager, EntityFieldManagerInterface $entity_field_manager, protected ?TimeInterface $time = NULL) {
    $this->manager = $manager;
    $this->entityFieldManager = $entity_field_manager;
    if ($this->time === NULL) {
      @trigger_error('Calling ' . __METHOD__ . ' without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3112298', E_USER_DEPRECATED);
      $this->time = \Drupal::service('datetime.time');
    }
  }
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('content_translation.manager'), $container->get('entity_field.manager'), $container->get('datetime.time'));
  }
  
  /**
   * Populates target values with the source values.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being translated.
   * @param \Drupal\Core\Language\LanguageInterface $source
   *   The language to be used as source.
   * @param \Drupal\Core\Language\LanguageInterface $target
   *   The language to be used as target.
   */
  public function prepareTranslation(ContentEntityInterface $entity, LanguageInterface $source, LanguageInterface $target) {
    $source_langcode = $source->getId();
    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
    $storage = $this->entityTypeManager()
      ->getStorage($entity->getEntityTypeId());
    // Once translations from the default revision are added, there may be
    // additional draft translations that don't exist in the default revision.
    // Add those translations too so that they aren't deleted when the new
    // translation is saved.
    /** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
    $default_revision = $storage->load($entity->id());
    // Check the entity isn't missing any translations.
    $languages = $this->languageManager()
      ->getLanguages();
    foreach ($languages as $language) {
      $langcode = $language->getId();
      if ($entity->hasTranslation($langcode) || $target->getId() === $langcode) {
        continue;
      }
      $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
      if ($latest_revision_id) {
        if ($default_revision->hasTranslation($langcode)) {
          $existing_translation = $default_revision->getTranslation($langcode);
          $existing_translation->setNewRevision(FALSE);
          $existing_translation->isDefaultRevision(FALSE);
          $existing_translation->setRevisionTranslationAffected(FALSE);
          $entity->addTranslation($langcode, $existing_translation->toArray());
        }
      }
    }
    /** @var \Drupal\Core\Entity\ContentEntityInterface $source_translation */
    $source_translation = $entity->getTranslation($source_langcode);
    $target_translation = $entity->addTranslation($target->getId(), $source_translation->toArray());
    // Make sure we do not inherit the affected status from the source values.
    if ($entity->getEntityType()
      ->isRevisionable()) {
      $target_translation->setRevisionTranslationAffected(NULL);
    }
    /** @var \Drupal\user\UserInterface $user */
    $user = $this->entityTypeManager()
      ->getStorage('user')
      ->load($this->currentUser()
      ->id());
    $metadata = $this->manager
      ->getTranslationMetadata($target_translation);
    // Update the translation author to current user, as well the translation
    // creation time.
    $metadata->setAuthor($user);
    $metadata->setCreatedTime($this->time
      ->getRequestTime());
    $metadata->setSource($source_langcode);
  }
  
  /**
   * 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.
   *
   * @return array
   *   Array of page elements to render.
   */
  public function overview(RouteMatchInterface $route_match, $entity_type_id = NULL) {
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $route_match->getParameter($entity_type_id);
    $account = $this->currentUser();
    $handler = $this->entityTypeManager()
      ->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->entityFieldManager
        ->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();
        }
        $options = [
          'language' => $language,
        ];
        $add_url = $entity->toUrl('drupal:content-translation-add', $options)
          ->setRouteParameter('source', $original)
          ->setRouteParameter('target', $language->getId());
        $edit_url = $entity->toUrl('drupal:content-translation-edit', $options)
          ->setRouteParameter('language', $language->getId());
        $delete_url = $entity->toUrl('drupal:content-translation-delete', $options)
          ->setRouteParameter('language', $language->getId());
        $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() ?? $entity->id();
          $link = [
            'url' => $entity->toUrl(),
          ];
          if (!empty($link['url'])) {
            $link['url']->setOption('language', $language);
            $row_title = Link::fromTextAndUrl($label, $link['url'])->toString();
          }
          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');
          $cacheability = $cacheability->merge(CacheableMetadata::createFromObject($update_access))
            ->merge(CacheableMetadata::createFromObject($translation_access));
          if ($update_access->isAllowed() && $entity_type->hasLinkTemplate('edit-form')) {
            $links['edit']['url'] = $entity->toUrl('edit-form');
            $links['edit']['language'] = $language;
          }
          elseif (!$is_original && $translation_access->isAllowed()) {
            $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 = \Drupal::service('content_translation.delete_access')->checkAccess($translation);
            $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->toUrl('delete-form'),
                  'language' => $language,
                ];
              }
              elseif ($translation_access->isAllowed()) {
                $links['delete'] = [
                  'title' => $this->t('Delete'),
                  'url' => $delete_url,
                ];
              }
            }
            else {
              $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() ?? $entity->id(),
    ]);
    // 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;
  }
  
  /**
   * Builds an add translation page.
   *
   * @param \Drupal\Core\Language\LanguageInterface $source
   *   The language of the values being translated. Defaults to the entity
   *   language.
   * @param \Drupal\Core\Language\LanguageInterface $target
   *   The language of the translated values. Defaults to the current content
   *   language.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The route match object from which to extract the entity type.
   * @param string $entity_type_id
   *   (optional) The entity type ID.
   *
   * @return array
   *   A processed form array ready to be rendered.
   */
  public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) {
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $route_match->getParameter($entity_type_id);
    // In case of a pending revision, make sure we load the latest
    // translation-affecting revision for the source language, otherwise the
    // initial form values may not be up-to-date.
    if (!$entity->isDefaultRevision() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) {
      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
      $storage = $this->entityTypeManager()
        ->getStorage($entity->getEntityTypeId());
      $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $source->getId());
      if ($revision_id != $entity->getRevisionId()) {
        $entity = $storage->loadRevision($revision_id);
      }
    }
    // @todo Exploit the upcoming hook_entity_prepare() when available.
    // See https://www.drupal.org/node/1810394.
    $this->prepareTranslation($entity, $source, $target);
    // @todo Provide a way to figure out the default form operation. Maybe like
    //   $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
    //   See https://www.drupal.org/node/2006348.
    // Use the add form handler, if available, otherwise default.
    $operation = $entity->getEntityType()
      ->hasHandlerClass('form', 'add') ? 'add' : 'default';
    $form_state_additions = [];
    $form_state_additions['langcode'] = $target->getId();
    $form_state_additions['content_translation']['source'] = $source;
    $form_state_additions['content_translation']['target'] = $target;
    $form_state_additions['content_translation']['translation_form'] = !$entity->access('update');
    return $this->entityFormBuilder()
      ->getForm($entity, $operation, $form_state_additions);
  }
  
  /**
   * Builds the edit translation page.
   *
   * @param \Drupal\Core\Language\LanguageInterface $language
   *   The language of the translated values. Defaults to the current content
   *   language.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The route match object from which to extract the entity type.
   * @param string $entity_type_id
   *   (optional) The entity type ID.
   *
   * @return array
   *   A processed form array ready to be rendered.
   */
  public function edit(LanguageInterface $language, RouteMatchInterface $route_match, $entity_type_id = NULL) {
    $entity = $route_match->getParameter($entity_type_id);
    // @todo Provide a way to figure out the default form operation. Maybe like
    //   $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
    //   See https://www.drupal.org/node/2006348.
    // Use the edit form handler, if available, otherwise default.
    $operation = $entity->getEntityType()
      ->hasHandlerClass('form', 'edit') ? 'edit' : 'default';
    $form_state_additions = [];
    $form_state_additions['langcode'] = $language->getId();
    $form_state_additions['content_translation']['translation_form'] = TRUE;
    return $this->entityFormBuilder()
      ->getForm($entity, $operation, $form_state_additions);
  }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
ContentTranslationController::$entityFieldManager protected property The entity field manager.
ContentTranslationController::$manager protected property The content translation manager.
ContentTranslationController::add public function Builds an add translation page.
ContentTranslationController::create public static function Instantiates a new instance of the implementing class using autowiring. Overrides AutowireTrait::create
ContentTranslationController::edit public function Builds the edit translation page.
ContentTranslationController::overview public function Builds the translations overview page.
ContentTranslationController::prepareTranslation public function Populates target values with the source values.
ContentTranslationController::__construct public function Initializes a content translation controller.
ControllerBase::$configFactory protected property The configuration factory.
ControllerBase::$currentUser protected property The current user service. 2
ControllerBase::$entityFormBuilder protected property The entity form builder.
ControllerBase::$entityTypeManager protected property The entity type manager.
ControllerBase::$formBuilder protected property The form builder. 1
ControllerBase::$keyValue protected property The key-value storage. 1
ControllerBase::$languageManager protected property The language manager. 1
ControllerBase::$moduleHandler protected property The module handler. 1
ControllerBase::$stateService protected property The state service.
ControllerBase::cache protected function Returns the requested cache bin.
ControllerBase::config protected function Retrieves a configuration object.
ControllerBase::container private function Returns the service container.
ControllerBase::currentUser protected function Returns the current user. 2
ControllerBase::entityFormBuilder protected function Retrieves the entity form builder.
ControllerBase::entityTypeManager protected function Retrieves the entity type manager.
ControllerBase::formBuilder protected function Returns the form builder service. 1
ControllerBase::keyValue protected function Returns a key/value storage collection. 1
ControllerBase::languageManager protected function Returns the language manager service. 1
ControllerBase::moduleHandler protected function Returns the module handler. 1
ControllerBase::redirect protected function Returns a redirect response object for the specified route.
ControllerBase::state protected function Returns the state storage service.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 16
MessengerTrait::messenger public function Gets the messenger. 16
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 2
RedirectDestinationTrait::getDestinationArray protected function Prepares a &#039;destination&#039; URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.