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: }