Chris@0: manager = $manager; Chris@18: if (!$entity_field_manager) { Chris@18: @trigger_error('The entity_field.manager service must be passed to ContentTranslationController::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); Chris@18: $entity_field_manager = \Drupal::service('entity_field.manager'); Chris@18: } Chris@18: $this->entityFieldManager = $entity_field_manager; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function create(ContainerInterface $container) { Chris@18: return new static( Chris@18: $container->get('content_translation.manager'), Chris@18: $container->get('entity_field.manager') Chris@18: ); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Populates target values with the source values. Chris@0: * Chris@0: * @param \Drupal\Core\Entity\ContentEntityInterface $entity Chris@0: * The entity being translated. Chris@0: * @param \Drupal\Core\Language\LanguageInterface $source Chris@0: * The language to be used as source. Chris@0: * @param \Drupal\Core\Language\LanguageInterface $target Chris@0: * The language to be used as target. Chris@0: */ Chris@0: public function prepareTranslation(ContentEntityInterface $entity, LanguageInterface $source, LanguageInterface $target) { Chris@0: /* @var \Drupal\Core\Entity\ContentEntityInterface $source_translation */ Chris@0: $source_translation = $entity->getTranslation($source->getId()); Chris@0: $target_translation = $entity->addTranslation($target->getId(), $source_translation->toArray()); Chris@0: Chris@0: // Make sure we do not inherit the affected status from the source values. Chris@0: if ($entity->getEntityType()->isRevisionable()) { Chris@0: $target_translation->setRevisionTranslationAffected(NULL); Chris@0: } Chris@0: Chris@0: /** @var \Drupal\user\UserInterface $user */ Chris@18: $user = $this->entityTypeManager()->getStorage('user')->load($this->currentUser()->id()); Chris@0: $metadata = $this->manager->getTranslationMetadata($target_translation); Chris@0: Chris@0: // Update the translation author to current user, as well the translation Chris@0: // creation time. Chris@0: $metadata->setAuthor($user); Chris@0: $metadata->setCreatedTime(REQUEST_TIME); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Builds the translations overview page. Chris@0: * Chris@0: * @param \Drupal\Core\Routing\RouteMatchInterface $route_match Chris@0: * The route match. Chris@0: * @param string $entity_type_id Chris@0: * (optional) The entity type ID. Chris@0: * @return array Chris@0: * Array of page elements to render. Chris@0: */ Chris@0: public function overview(RouteMatchInterface $route_match, $entity_type_id = NULL) { Chris@0: /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ Chris@0: $entity = $route_match->getParameter($entity_type_id); Chris@0: $account = $this->currentUser(); Chris@18: $handler = $this->entityTypeManager()->getHandler($entity_type_id, 'translation'); Chris@0: $manager = $this->manager; Chris@0: $entity_type = $entity->getEntityType(); Chris@14: $use_latest_revisions = $entity_type->isRevisionable() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle()); Chris@0: Chris@0: // Start collecting the cacheability metadata, starting with the entity and Chris@0: // later merge in the access result cacheability metadata. Chris@0: $cacheability = CacheableMetadata::createFromObject($entity); Chris@0: Chris@0: $languages = $this->languageManager()->getLanguages(); Chris@0: $original = $entity->getUntranslated()->language()->getId(); Chris@0: $translations = $entity->getTranslationLanguages(); Chris@0: $field_ui = $this->moduleHandler()->moduleExists('field_ui') && $account->hasPermission('administer ' . $entity_type_id . ' fields'); Chris@0: Chris@0: $rows = []; Chris@0: $show_source_column = FALSE; Chris@14: /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ Chris@14: $storage = $this->entityTypeManager()->getStorage($entity_type_id); Chris@14: $default_revision = $storage->load($entity->id()); Chris@0: Chris@0: if ($this->languageManager()->isMultilingual()) { Chris@0: // Determine whether the current entity is translatable. Chris@0: $translatable = FALSE; Chris@18: foreach ($this->entityFieldManager->getFieldDefinitions($entity_type_id, $entity->bundle()) as $instance) { Chris@0: if ($instance->isTranslatable()) { Chris@0: $translatable = TRUE; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: // Show source-language column if there are non-original source langcodes. Chris@0: $additional_source_langcodes = array_filter(array_keys($translations), function ($langcode) use ($entity, $original, $manager) { Chris@0: $source = $manager->getTranslationMetadata($entity->getTranslation($langcode))->getSource(); Chris@0: return $source != $original && $source != LanguageInterface::LANGCODE_NOT_SPECIFIED; Chris@0: }); Chris@0: $show_source_column = !empty($additional_source_langcodes); Chris@0: Chris@0: foreach ($languages as $language) { Chris@0: $language_name = $language->getName(); Chris@0: $langcode = $language->getId(); Chris@0: Chris@14: // If the entity type is revisionable, we may have pending revisions Chris@14: // with translations not available yet in the default revision. Thus we Chris@14: // need to load the latest translation-affecting revision for each Chris@14: // language to be sure we are listing all available translations. Chris@14: if ($use_latest_revisions) { Chris@14: $entity = $default_revision; Chris@14: $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode); Chris@14: if ($latest_revision_id) { Chris@14: /** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */ Chris@14: $latest_revision = $storage->loadRevision($latest_revision_id); Chris@14: // Make sure we do not list removed translations, i.e. translations Chris@14: // that have been part of a default revision but no longer are. Chris@14: if (!$latest_revision->wasDefaultRevision() || $default_revision->hasTranslation($langcode)) { Chris@14: $entity = $latest_revision; Chris@14: } Chris@14: } Chris@14: $translations = $entity->getTranslationLanguages(); Chris@14: } Chris@14: Chris@17: $options = ['language' => $language]; Chris@17: $add_url = $entity->toUrl('drupal:content-translation-add', $options) Chris@17: ->setRouteParameter('source', $original) Chris@17: ->setRouteParameter('target', $language->getId()); Chris@17: $edit_url = $entity->toUrl('drupal:content-translation-edit', $options) Chris@17: ->setRouteParameter('language', $language->getId()); Chris@17: $delete_url = $entity->toUrl('drupal:content-translation-delete', $options) Chris@17: ->setRouteParameter('language', $language->getId()); Chris@0: $operations = [ Chris@0: 'data' => [ Chris@0: '#type' => 'operations', Chris@0: '#links' => [], Chris@0: ], Chris@0: ]; Chris@0: Chris@0: $links = &$operations['data']['#links']; Chris@0: if (array_key_exists($langcode, $translations)) { Chris@0: // Existing translation in the translation set: display status. Chris@0: $translation = $entity->getTranslation($langcode); Chris@0: $metadata = $manager->getTranslationMetadata($translation); Chris@0: $source = $metadata->getSource() ?: LanguageInterface::LANGCODE_NOT_SPECIFIED; Chris@0: $is_original = $langcode == $original; Chris@0: $label = $entity->getTranslation($langcode)->label(); Chris@18: $link = isset($links->links[$langcode]['url']) ? $links->links[$langcode] : ['url' => $entity->toUrl()]; Chris@0: if (!empty($link['url'])) { Chris@0: $link['url']->setOption('language', $language); Chris@0: $row_title = $this->l($label, $link['url']); Chris@0: } Chris@0: Chris@0: if (empty($link['url'])) { Chris@0: $row_title = $is_original ? $label : $this->t('n/a'); Chris@0: } Chris@0: Chris@0: // If the user is allowed to edit the entity we point the edit link to Chris@0: // the entity form, otherwise if we are not dealing with the original Chris@0: // language we point the link to the translation form. Chris@0: $update_access = $entity->access('update', NULL, TRUE); Chris@0: $translation_access = $handler->getTranslationAccess($entity, 'update'); Chris@0: $cacheability = $cacheability Chris@0: ->merge(CacheableMetadata::createFromObject($update_access)) Chris@0: ->merge(CacheableMetadata::createFromObject($translation_access)); Chris@0: if ($update_access->isAllowed() && $entity_type->hasLinkTemplate('edit-form')) { Chris@18: $links['edit']['url'] = $entity->toUrl('edit-form'); Chris@0: $links['edit']['language'] = $language; Chris@0: } Chris@0: elseif (!$is_original && $translation_access->isAllowed()) { Chris@0: $links['edit']['url'] = $edit_url; Chris@0: } Chris@0: Chris@0: if (isset($links['edit'])) { Chris@0: $links['edit']['title'] = $this->t('Edit'); Chris@0: } Chris@0: $status = [ Chris@0: 'data' => [ Chris@0: '#type' => 'inline_template', Chris@0: '#template' => '{% if status %}{{ "Published"|t }}{% else %}{{ "Not published"|t }}{% endif %}{% if outdated %} {{ "outdated"|t }}{% endif %}', Chris@0: '#context' => [ Chris@0: 'status' => $metadata->isPublished(), Chris@0: 'outdated' => $metadata->isOutdated(), Chris@0: ], Chris@0: ], Chris@0: ]; Chris@0: Chris@0: if ($is_original) { Chris@0: $language_name = $this->t('@language_name (Original language)', ['@language_name' => $language_name]); Chris@0: $source_name = $this->t('n/a'); Chris@0: } Chris@0: else { Chris@14: /** @var \Drupal\Core\Access\AccessResultInterface $delete_route_access */ Chris@14: $delete_route_access = \Drupal::service('content_translation.delete_access')->checkAccess($translation); Chris@14: $cacheability->addCacheableDependency($delete_route_access); Chris@14: Chris@14: if ($delete_route_access->isAllowed()) { Chris@14: $source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a'); Chris@14: $delete_access = $entity->access('delete', NULL, TRUE); Chris@14: $translation_access = $handler->getTranslationAccess($entity, 'delete'); Chris@14: $cacheability Chris@14: ->addCacheableDependency($delete_access) Chris@14: ->addCacheableDependency($translation_access); Chris@14: Chris@14: if ($delete_access->isAllowed() && $entity_type->hasLinkTemplate('delete-form')) { Chris@14: $links['delete'] = [ Chris@14: 'title' => $this->t('Delete'), Chris@18: 'url' => $entity->toUrl('delete-form'), Chris@14: 'language' => $language, Chris@14: ]; Chris@14: } Chris@14: elseif ($translation_access->isAllowed()) { Chris@14: $links['delete'] = [ Chris@14: 'title' => $this->t('Delete'), Chris@14: 'url' => $delete_url, Chris@14: ]; Chris@14: } Chris@0: } Chris@14: else { Chris@14: $this->messenger()->addWarning($this->t('The "Delete translation" action is only available for published translations.'), FALSE); Chris@0: } Chris@0: } Chris@0: } Chris@0: else { Chris@0: // No such translation in the set yet: help user to create it. Chris@0: $row_title = $source_name = $this->t('n/a'); Chris@0: $source = $entity->language()->getId(); Chris@0: Chris@0: $create_translation_access = $handler->getTranslationAccess($entity, 'create'); Chris@0: $cacheability = $cacheability Chris@0: ->merge(CacheableMetadata::createFromObject($create_translation_access)); Chris@0: if ($source != $langcode && $create_translation_access->isAllowed()) { Chris@0: if ($translatable) { Chris@0: $links['add'] = [ Chris@0: 'title' => $this->t('Add'), Chris@0: 'url' => $add_url, Chris@0: ]; Chris@0: } Chris@0: elseif ($field_ui) { Chris@0: $url = new Url('language.content_settings_page'); Chris@0: Chris@0: // Link directly to the fields tab to make it easier to find the Chris@0: // setting to enable translation on fields. Chris@0: $links['nofields'] = [ Chris@0: 'title' => $this->t('No translatable fields'), Chris@0: 'url' => $url, Chris@0: ]; Chris@0: } Chris@0: } Chris@0: Chris@0: $status = $this->t('Not translated'); Chris@0: } Chris@0: if ($show_source_column) { Chris@0: $rows[] = [ Chris@0: $language_name, Chris@0: $row_title, Chris@0: $source_name, Chris@0: $status, Chris@0: $operations, Chris@0: ]; Chris@0: } Chris@0: else { Chris@0: $rows[] = [$language_name, $row_title, $status, $operations]; Chris@0: } Chris@0: } Chris@0: } Chris@0: if ($show_source_column) { Chris@0: $header = [ Chris@0: $this->t('Language'), Chris@0: $this->t('Translation'), Chris@0: $this->t('Source language'), Chris@0: $this->t('Status'), Chris@0: $this->t('Operations'), Chris@0: ]; Chris@0: } Chris@0: else { Chris@0: $header = [ Chris@0: $this->t('Language'), Chris@0: $this->t('Translation'), Chris@0: $this->t('Status'), Chris@0: $this->t('Operations'), Chris@0: ]; Chris@0: } Chris@0: Chris@0: $build['#title'] = $this->t('Translations of %label', ['%label' => $entity->label()]); Chris@0: Chris@0: // Add metadata to the build render array to let other modules know about Chris@0: // which entity this is. Chris@0: $build['#entity'] = $entity; Chris@0: $cacheability Chris@0: ->addCacheTags($entity->getCacheTags()) Chris@0: ->applyTo($build); Chris@0: Chris@0: $build['content_translation_overview'] = [ Chris@0: '#theme' => 'table', Chris@0: '#header' => $header, Chris@0: '#rows' => $rows, Chris@0: ]; Chris@0: Chris@0: return $build; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Builds an add translation page. Chris@0: * Chris@0: * @param \Drupal\Core\Language\LanguageInterface $source Chris@0: * The language of the values being translated. Defaults to the entity Chris@0: * language. Chris@0: * @param \Drupal\Core\Language\LanguageInterface $target Chris@0: * The language of the translated values. Defaults to the current content Chris@0: * language. Chris@0: * @param \Drupal\Core\Routing\RouteMatchInterface $route_match Chris@0: * The route match object from which to extract the entity type. Chris@0: * @param string $entity_type_id Chris@0: * (optional) The entity type ID. Chris@0: * Chris@0: * @return array Chris@0: * A processed form array ready to be rendered. Chris@0: */ Chris@0: public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) { Chris@14: /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ Chris@0: $entity = $route_match->getParameter($entity_type_id); Chris@0: Chris@14: // In case of a pending revision, make sure we load the latest Chris@14: // translation-affecting revision for the source language, otherwise the Chris@14: // initial form values may not be up-to-date. Chris@14: if (!$entity->isDefaultRevision() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) { Chris@14: /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ Chris@14: $storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId()); Chris@14: $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $source->getId()); Chris@14: if ($revision_id != $entity->getRevisionId()) { Chris@14: $entity = $storage->loadRevision($revision_id); Chris@14: } Chris@14: } Chris@14: Chris@0: // @todo Exploit the upcoming hook_entity_prepare() when available. Chris@0: // See https://www.drupal.org/node/1810394. Chris@0: $this->prepareTranslation($entity, $source, $target); Chris@0: Chris@0: // @todo Provide a way to figure out the default form operation. Maybe like Chris@0: // $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default'; Chris@0: // See https://www.drupal.org/node/2006348. Chris@0: Chris@0: // Use the add form handler, if available, otherwise default. Chris@0: $operation = $entity->getEntityType()->hasHandlerClass('form', 'add') ? 'add' : 'default'; Chris@0: Chris@0: $form_state_additions = []; Chris@0: $form_state_additions['langcode'] = $target->getId(); Chris@0: $form_state_additions['content_translation']['source'] = $source; Chris@0: $form_state_additions['content_translation']['target'] = $target; Chris@0: $form_state_additions['content_translation']['translation_form'] = !$entity->access('update'); Chris@0: Chris@0: return $this->entityFormBuilder()->getForm($entity, $operation, $form_state_additions); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Builds the edit translation page. Chris@0: * Chris@0: * @param \Drupal\Core\Language\LanguageInterface $language Chris@0: * The language of the translated values. Defaults to the current content Chris@0: * language. Chris@0: * @param \Drupal\Core\Routing\RouteMatchInterface $route_match Chris@0: * The route match object from which to extract the entity type. Chris@0: * @param string $entity_type_id Chris@0: * (optional) The entity type ID. Chris@0: * Chris@0: * @return array Chris@0: * A processed form array ready to be rendered. Chris@0: */ Chris@0: public function edit(LanguageInterface $language, RouteMatchInterface $route_match, $entity_type_id = NULL) { Chris@0: $entity = $route_match->getParameter($entity_type_id); Chris@0: Chris@0: // @todo Provide a way to figure out the default form operation. Maybe like Chris@0: // $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default'; Chris@0: // See https://www.drupal.org/node/2006348. Chris@0: Chris@0: // Use the edit form handler, if available, otherwise default. Chris@0: $operation = $entity->getEntityType()->hasHandlerClass('form', 'edit') ? 'edit' : 'default'; Chris@0: Chris@0: $form_state_additions = []; Chris@0: $form_state_additions['langcode'] = $language->getId(); Chris@0: $form_state_additions['content_translation']['translation_form'] = TRUE; Chris@0: Chris@0: return $this->entityFormBuilder()->getForm($entity, $operation, $form_state_additions); Chris@0: } Chris@0: Chris@0: }