3 namespace Drupal\content_translation\Controller;
5 use Drupal\content_translation\ContentTranslationManager;
6 use Drupal\content_translation\ContentTranslationManagerInterface;
7 use Drupal\Core\Cache\CacheableMetadata;
8 use Drupal\Core\Controller\ControllerBase;
9 use Drupal\Core\Entity\ContentEntityInterface;
10 use Drupal\Core\Language\LanguageInterface;
11 use Drupal\Core\Routing\RouteMatchInterface;
13 use Symfony\Component\DependencyInjection\ContainerInterface;
16 * Base class for entity translation controllers.
18 class ContentTranslationController extends ControllerBase {
21 * The content translation manager.
23 * @var \Drupal\content_translation\ContentTranslationManagerInterface
28 * Initializes a content translation controller.
30 * @param \Drupal\content_translation\ContentTranslationManagerInterface $manager
31 * A content translation manager instance.
33 public function __construct(ContentTranslationManagerInterface $manager) {
34 $this->manager = $manager;
40 public static function create(ContainerInterface $container) {
41 return new static($container->get('content_translation.manager'));
45 * Populates target values with the source values.
47 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
48 * The entity being translated.
49 * @param \Drupal\Core\Language\LanguageInterface $source
50 * The language to be used as source.
51 * @param \Drupal\Core\Language\LanguageInterface $target
52 * The language to be used as target.
54 public function prepareTranslation(ContentEntityInterface $entity, LanguageInterface $source, LanguageInterface $target) {
55 /* @var \Drupal\Core\Entity\ContentEntityInterface $source_translation */
56 $source_translation = $entity->getTranslation($source->getId());
57 $target_translation = $entity->addTranslation($target->getId(), $source_translation->toArray());
59 // Make sure we do not inherit the affected status from the source values.
60 if ($entity->getEntityType()->isRevisionable()) {
61 $target_translation->setRevisionTranslationAffected(NULL);
64 /** @var \Drupal\user\UserInterface $user */
65 $user = $this->entityManager()->getStorage('user')->load($this->currentUser()->id());
66 $metadata = $this->manager->getTranslationMetadata($target_translation);
68 // Update the translation author to current user, as well the translation
70 $metadata->setAuthor($user);
71 $metadata->setCreatedTime(REQUEST_TIME);
75 * Builds the translations overview page.
77 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
79 * @param string $entity_type_id
80 * (optional) The entity type ID.
82 * Array of page elements to render.
84 public function overview(RouteMatchInterface $route_match, $entity_type_id = NULL) {
85 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
86 $entity = $route_match->getParameter($entity_type_id);
87 $account = $this->currentUser();
88 $handler = $this->entityManager()->getHandler($entity_type_id, 'translation');
89 $manager = $this->manager;
90 $entity_type = $entity->getEntityType();
91 $use_latest_revisions = $entity_type->isRevisionable() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle());
93 // Start collecting the cacheability metadata, starting with the entity and
94 // later merge in the access result cacheability metadata.
95 $cacheability = CacheableMetadata::createFromObject($entity);
97 $languages = $this->languageManager()->getLanguages();
98 $original = $entity->getUntranslated()->language()->getId();
99 $translations = $entity->getTranslationLanguages();
100 $field_ui = $this->moduleHandler()->moduleExists('field_ui') && $account->hasPermission('administer ' . $entity_type_id . ' fields');
103 $show_source_column = FALSE;
104 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
105 $storage = $this->entityTypeManager()->getStorage($entity_type_id);
106 $default_revision = $storage->load($entity->id());
108 if ($this->languageManager()->isMultilingual()) {
109 // Determine whether the current entity is translatable.
110 $translatable = FALSE;
111 foreach ($this->entityManager->getFieldDefinitions($entity_type_id, $entity->bundle()) as $instance) {
112 if ($instance->isTranslatable()) {
113 $translatable = TRUE;
118 // Show source-language column if there are non-original source langcodes.
119 $additional_source_langcodes = array_filter(array_keys($translations), function ($langcode) use ($entity, $original, $manager) {
120 $source = $manager->getTranslationMetadata($entity->getTranslation($langcode))->getSource();
121 return $source != $original && $source != LanguageInterface::LANGCODE_NOT_SPECIFIED;
123 $show_source_column = !empty($additional_source_langcodes);
125 foreach ($languages as $language) {
126 $language_name = $language->getName();
127 $langcode = $language->getId();
129 // If the entity type is revisionable, we may have pending revisions
130 // with translations not available yet in the default revision. Thus we
131 // need to load the latest translation-affecting revision for each
132 // language to be sure we are listing all available translations.
133 if ($use_latest_revisions) {
134 $entity = $default_revision;
135 $latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
136 if ($latest_revision_id) {
137 /** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */
138 $latest_revision = $storage->loadRevision($latest_revision_id);
139 // Make sure we do not list removed translations, i.e. translations
140 // that have been part of a default revision but no longer are.
141 if (!$latest_revision->wasDefaultRevision() || $default_revision->hasTranslation($langcode)) {
142 $entity = $latest_revision;
145 $translations = $entity->getTranslationLanguages();
148 $options = ['language' => $language];
149 $add_url = $entity->toUrl('drupal:content-translation-add', $options)
150 ->setRouteParameter('source', $original)
151 ->setRouteParameter('target', $language->getId());
152 $edit_url = $entity->toUrl('drupal:content-translation-edit', $options)
153 ->setRouteParameter('language', $language->getId());
154 $delete_url = $entity->toUrl('drupal:content-translation-delete', $options)
155 ->setRouteParameter('language', $language->getId());
158 '#type' => 'operations',
163 $links = &$operations['data']['#links'];
164 if (array_key_exists($langcode, $translations)) {
165 // Existing translation in the translation set: display status.
166 $translation = $entity->getTranslation($langcode);
167 $metadata = $manager->getTranslationMetadata($translation);
168 $source = $metadata->getSource() ?: LanguageInterface::LANGCODE_NOT_SPECIFIED;
169 $is_original = $langcode == $original;
170 $label = $entity->getTranslation($langcode)->label();
171 $link = isset($links->links[$langcode]['url']) ? $links->links[$langcode] : ['url' => $entity->urlInfo()];
172 if (!empty($link['url'])) {
173 $link['url']->setOption('language', $language);
174 $row_title = $this->l($label, $link['url']);
177 if (empty($link['url'])) {
178 $row_title = $is_original ? $label : $this->t('n/a');
181 // If the user is allowed to edit the entity we point the edit link to
182 // the entity form, otherwise if we are not dealing with the original
183 // language we point the link to the translation form.
184 $update_access = $entity->access('update', NULL, TRUE);
185 $translation_access = $handler->getTranslationAccess($entity, 'update');
186 $cacheability = $cacheability
187 ->merge(CacheableMetadata::createFromObject($update_access))
188 ->merge(CacheableMetadata::createFromObject($translation_access));
189 if ($update_access->isAllowed() && $entity_type->hasLinkTemplate('edit-form')) {
190 $links['edit']['url'] = $entity->urlInfo('edit-form');
191 $links['edit']['language'] = $language;
193 elseif (!$is_original && $translation_access->isAllowed()) {
194 $links['edit']['url'] = $edit_url;
197 if (isset($links['edit'])) {
198 $links['edit']['title'] = $this->t('Edit');
202 '#type' => 'inline_template',
203 '#template' => '<span class="status">{% if status %}{{ "Published"|t }}{% else %}{{ "Not published"|t }}{% endif %}</span>{% if outdated %} <span class="marker">{{ "outdated"|t }}</span>{% endif %}',
205 'status' => $metadata->isPublished(),
206 'outdated' => $metadata->isOutdated(),
212 $language_name = $this->t('<strong>@language_name (Original language)</strong>', ['@language_name' => $language_name]);
213 $source_name = $this->t('n/a');
216 /** @var \Drupal\Core\Access\AccessResultInterface $delete_route_access */
217 $delete_route_access = \Drupal::service('content_translation.delete_access')->checkAccess($translation);
218 $cacheability->addCacheableDependency($delete_route_access);
220 if ($delete_route_access->isAllowed()) {
221 $source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a');
222 $delete_access = $entity->access('delete', NULL, TRUE);
223 $translation_access = $handler->getTranslationAccess($entity, 'delete');
225 ->addCacheableDependency($delete_access)
226 ->addCacheableDependency($translation_access);
228 if ($delete_access->isAllowed() && $entity_type->hasLinkTemplate('delete-form')) {
230 'title' => $this->t('Delete'),
231 'url' => $entity->urlInfo('delete-form'),
232 'language' => $language,
235 elseif ($translation_access->isAllowed()) {
237 'title' => $this->t('Delete'),
238 'url' => $delete_url,
243 $this->messenger()->addWarning($this->t('The "Delete translation" action is only available for published translations.'), FALSE);
248 // No such translation in the set yet: help user to create it.
249 $row_title = $source_name = $this->t('n/a');
250 $source = $entity->language()->getId();
252 $create_translation_access = $handler->getTranslationAccess($entity, 'create');
253 $cacheability = $cacheability
254 ->merge(CacheableMetadata::createFromObject($create_translation_access));
255 if ($source != $langcode && $create_translation_access->isAllowed()) {
258 'title' => $this->t('Add'),
263 $url = new Url('language.content_settings_page');
265 // Link directly to the fields tab to make it easier to find the
266 // setting to enable translation on fields.
267 $links['nofields'] = [
268 'title' => $this->t('No translatable fields'),
274 $status = $this->t('Not translated');
276 if ($show_source_column) {
286 $rows[] = [$language_name, $row_title, $status, $operations];
290 if ($show_source_column) {
292 $this->t('Language'),
293 $this->t('Translation'),
294 $this->t('Source language'),
296 $this->t('Operations'),
301 $this->t('Language'),
302 $this->t('Translation'),
304 $this->t('Operations'),
308 $build['#title'] = $this->t('Translations of %label', ['%label' => $entity->label()]);
310 // Add metadata to the build render array to let other modules know about
311 // which entity this is.
312 $build['#entity'] = $entity;
314 ->addCacheTags($entity->getCacheTags())
317 $build['content_translation_overview'] = [
319 '#header' => $header,
327 * Builds an add translation page.
329 * @param \Drupal\Core\Language\LanguageInterface $source
330 * The language of the values being translated. Defaults to the entity
332 * @param \Drupal\Core\Language\LanguageInterface $target
333 * The language of the translated values. Defaults to the current content
335 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
336 * The route match object from which to extract the entity type.
337 * @param string $entity_type_id
338 * (optional) The entity type ID.
341 * A processed form array ready to be rendered.
343 public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) {
344 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
345 $entity = $route_match->getParameter($entity_type_id);
347 // In case of a pending revision, make sure we load the latest
348 // translation-affecting revision for the source language, otherwise the
349 // initial form values may not be up-to-date.
350 if (!$entity->isDefaultRevision() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) {
351 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
352 $storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId());
353 $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $source->getId());
354 if ($revision_id != $entity->getRevisionId()) {
355 $entity = $storage->loadRevision($revision_id);
359 // @todo Exploit the upcoming hook_entity_prepare() when available.
360 // See https://www.drupal.org/node/1810394.
361 $this->prepareTranslation($entity, $source, $target);
363 // @todo Provide a way to figure out the default form operation. Maybe like
364 // $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
365 // See https://www.drupal.org/node/2006348.
367 // Use the add form handler, if available, otherwise default.
368 $operation = $entity->getEntityType()->hasHandlerClass('form', 'add') ? 'add' : 'default';
370 $form_state_additions = [];
371 $form_state_additions['langcode'] = $target->getId();
372 $form_state_additions['content_translation']['source'] = $source;
373 $form_state_additions['content_translation']['target'] = $target;
374 $form_state_additions['content_translation']['translation_form'] = !$entity->access('update');
376 return $this->entityFormBuilder()->getForm($entity, $operation, $form_state_additions);
380 * Builds the edit translation page.
382 * @param \Drupal\Core\Language\LanguageInterface $language
383 * The language of the translated values. Defaults to the current content
385 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
386 * The route match object from which to extract the entity type.
387 * @param string $entity_type_id
388 * (optional) The entity type ID.
391 * A processed form array ready to be rendered.
393 public function edit(LanguageInterface $language, RouteMatchInterface $route_match, $entity_type_id = NULL) {
394 $entity = $route_match->getParameter($entity_type_id);
396 // @todo Provide a way to figure out the default form operation. Maybe like
397 // $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
398 // See https://www.drupal.org/node/2006348.
400 // Use the edit form handler, if available, otherwise default.
401 $operation = $entity->getEntityType()->hasHandlerClass('form', 'edit') ? 'edit' : 'default';
403 $form_state_additions = [];
404 $form_state_additions['langcode'] = $language->getId();
405 $form_state_additions['content_translation']['translation_form'] = TRUE;
407 return $this->entityFormBuilder()->getForm($entity, $operation, $form_state_additions);