Initial Drupal 11 with DDEV setup
This commit is contained in:
@ -0,0 +1,26 @@
|
||||
# Schema for the Content Translation module.
|
||||
|
||||
field.field.*.*.*.third_party.content_translation:
|
||||
type: mapping
|
||||
label: 'Content translation field settings'
|
||||
mapping:
|
||||
translation_sync:
|
||||
type: sequence
|
||||
label: 'Field properties for which to synchronize translations'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Field column for which to synchronize translations'
|
||||
|
||||
language.content_settings.*.*.third_party.content_translation:
|
||||
type: mapping
|
||||
label: 'Content translation content settings'
|
||||
mapping:
|
||||
enabled:
|
||||
type: boolean
|
||||
label: 'Content translation enabled'
|
||||
bundle_settings:
|
||||
type: sequence
|
||||
label: 'Content translation bundle settings'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Bundle settings values'
|
||||
@ -0,0 +1,9 @@
|
||||
# Schema for the views plugins of the Content Translation module.
|
||||
|
||||
views.field.content_translation_link:
|
||||
type: views_field
|
||||
label: 'Content translation link'
|
||||
mapping:
|
||||
text:
|
||||
type: label
|
||||
label: 'Text to display'
|
||||
@ -0,0 +1,405 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
*/
|
||||
|
||||
use Drupal\content_translation\BundleTranslationSettingsInterface;
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
|
||||
/**
|
||||
* Returns a form element to configure field synchronization.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
|
||||
* A field definition object.
|
||||
* @param string $element_name
|
||||
* (optional) The element name, which is added to drupalSettings so that
|
||||
* javascript can manipulate the form element.
|
||||
*
|
||||
* @return array
|
||||
* A form element to configure field synchronization.
|
||||
*/
|
||||
function content_translation_field_sync_widget(FieldDefinitionInterface $field, $element_name = 'third_party_settings[content_translation][translation_sync]'): array {
|
||||
// No way to store field sync information on this field.
|
||||
if (!($field instanceof ThirdPartySettingsInterface)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$element = [];
|
||||
$definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->getType());
|
||||
$column_groups = $definition['column_groups'];
|
||||
if (!empty($column_groups) && count($column_groups) > 1) {
|
||||
$options = [];
|
||||
$default = [];
|
||||
$require_all_groups_for_translation = [];
|
||||
|
||||
foreach ($column_groups as $group => $info) {
|
||||
$options[$group] = $info['label'];
|
||||
$default[$group] = !empty($info['translatable']) ? $group : FALSE;
|
||||
if (!empty($info['require_all_groups_for_translation'])) {
|
||||
$require_all_groups_for_translation[] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
$default = $field->getThirdPartySetting('content_translation', 'translation_sync', $default);
|
||||
|
||||
$element = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Translatable elements'),
|
||||
'#options' => $options,
|
||||
'#default_value' => $default,
|
||||
];
|
||||
|
||||
if ($require_all_groups_for_translation) {
|
||||
// The actual checkboxes are sometimes rendered separately and the parent
|
||||
// element is ignored. Attach to the first option to ensure that this
|
||||
// does not get lost.
|
||||
$element[key($options)]['#attached']['drupalSettings']['contentTranslationDependentOptions'] = [
|
||||
'dependent_selectors' => [
|
||||
$element_name => $require_all_groups_for_translation,
|
||||
],
|
||||
];
|
||||
$element[key($options)]['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
|
||||
}
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects translation settings into the content language settings form.
|
||||
*/
|
||||
function _content_translation_form_language_content_settings_form_alter(array &$form, FormStateInterface $form_state): void {
|
||||
// Inject into the content language settings the translation settings if the
|
||||
// user has the required permission.
|
||||
if (!\Drupal::currentUser()->hasPermission('administer content translation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
|
||||
$content_translation_manager = \Drupal::service('content_translation.manager');
|
||||
$default = $form['entity_types']['#default_value'];
|
||||
foreach ($default as $entity_type_id => $enabled) {
|
||||
$default[$entity_type_id] = $enabled || $content_translation_manager->isEnabled($entity_type_id) ? $entity_type_id : FALSE;
|
||||
}
|
||||
$form['entity_types']['#default_value'] = $default;
|
||||
|
||||
$form['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
|
||||
|
||||
$entity_type_manager = \Drupal::entityTypeManager();
|
||||
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
|
||||
$entity_field_manager = \Drupal::service('entity_field.manager');
|
||||
$bundle_info_service = \Drupal::service('entity_type.bundle.info');
|
||||
foreach ($form['#labels'] as $entity_type_id => $label) {
|
||||
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
|
||||
$storage_definitions = $entity_type instanceof ContentEntityTypeInterface ? $entity_field_manager->getFieldStorageDefinitions($entity_type_id) : [];
|
||||
|
||||
$entity_type_translatable = $content_translation_manager->isSupported($entity_type_id);
|
||||
foreach ($bundle_info_service->getBundleInfo($entity_type_id) as $bundle => $bundle_info) {
|
||||
// Here we do not want the widget to be altered and hold also the "Enable
|
||||
// translation" checkbox, which would be redundant. Hence we add this key
|
||||
// to be able to skip alterations. Alter the title and display the message
|
||||
// about UI integration.
|
||||
$form['settings'][$entity_type_id][$bundle]['settings']['language']['#content_translation_skip_alter'] = TRUE;
|
||||
if (!$entity_type_translatable) {
|
||||
$form['settings'][$entity_type_id]['#title'] = t('@label (Translation is not supported).', ['@label' => $entity_type->getLabel()]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Displayed the "shared fields widgets" toggle.
|
||||
if ($content_translation_manager instanceof BundleTranslationSettingsInterface) {
|
||||
$settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
|
||||
$force_hidden = ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
|
||||
$form['settings'][$entity_type_id][$bundle]['settings']['content_translation']['untranslatable_fields_hide'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Hide non translatable fields on translation forms'),
|
||||
'#default_value' => $force_hidden || !empty($settings['untranslatable_fields_hide']),
|
||||
'#disabled' => $force_hidden,
|
||||
'#description' => $force_hidden ? t('Moderated content requires non-translatable fields to be edited in the original language form.') : '',
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name="settings[' . $entity_type_id . '][' . $bundle . '][translatable]"]' => [
|
||||
'checked' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$fields = $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle);
|
||||
if ($fields) {
|
||||
foreach ($fields as $field_name => $definition) {
|
||||
if ($definition->isComputed() || (!empty($storage_definitions[$field_name]) && _content_translation_is_field_translatability_configurable($entity_type, $storage_definitions[$field_name]))) {
|
||||
$form['settings'][$entity_type_id][$bundle]['fields'][$field_name] = [
|
||||
'#label' => $definition->getLabel(),
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $definition->isTranslatable(),
|
||||
];
|
||||
// Display the column translatability configuration widget.
|
||||
$column_element = content_translation_field_sync_widget($definition, "settings[{$entity_type_id}][{$bundle}][columns][{$field_name}]");
|
||||
if ($column_element) {
|
||||
$form['settings'][$entity_type_id][$bundle]['columns'][$field_name] = $column_element;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($form['settings'][$entity_type_id][$bundle]['fields'])) {
|
||||
// Only show the checkbox to enable translation if the bundles in the
|
||||
// entity might have fields and if there are fields to translate.
|
||||
$form['settings'][$entity_type_id][$bundle]['translatable'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $content_translation_manager->isEnabled($entity_type_id, $bundle),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$form['#validate'][] = 'content_translation_form_language_content_settings_validate';
|
||||
$form['#submit'][] = 'content_translation_form_language_content_settings_submit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether translatability should be configurable for a field.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
|
||||
* The field storage definition.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if field translatability can be configured, FALSE otherwise.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function _content_translation_is_field_translatability_configurable(EntityTypeInterface $entity_type, FieldStorageDefinitionInterface $definition) {
|
||||
// Allow to configure only fields supporting multilingual storage. We skip our
|
||||
// own fields as they are always translatable. Additionally we skip a set of
|
||||
// well-known fields implementing entity system business logic.
|
||||
return $definition->isTranslatable() &&
|
||||
$definition->getProvider() != 'content_translation' &&
|
||||
!in_array($definition->getName(), [$entity_type->getKey('langcode'), $entity_type->getKey('default_langcode'), 'revision_translation_affected']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects translation settings into the language content settings table.
|
||||
*/
|
||||
function _content_translation_preprocess_language_content_settings_table(&$variables): void {
|
||||
// Alter the 'build' variable injecting the translation settings if the user
|
||||
// has the required permission.
|
||||
if (!\Drupal::currentUser()->hasPermission('administer content translation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$element = $variables['element'];
|
||||
$build = &$variables['build'];
|
||||
|
||||
array_unshift($build['#header'], ['data' => t('Translatable'), 'class' => ['translatable']]);
|
||||
$rows = [];
|
||||
|
||||
foreach (Element::children($element) as $bundle) {
|
||||
$field_names = !empty($element[$bundle]['fields']) ? Element::children($element[$bundle]['fields']) : [];
|
||||
if (!empty($element[$bundle]['translatable'])) {
|
||||
$checkbox_id = $element[$bundle]['translatable']['#id'];
|
||||
}
|
||||
$rows[$bundle] = $build['#rows'][$bundle];
|
||||
|
||||
if (!empty($element[$bundle]['translatable'])) {
|
||||
$translatable = [
|
||||
'data' => $element[$bundle]['translatable'],
|
||||
'class' => ['translatable'],
|
||||
];
|
||||
array_unshift($rows[$bundle]['data'], $translatable);
|
||||
|
||||
$rows[$bundle]['data'][1]['data']['#prefix'] = '<label for="' . $checkbox_id . '">';
|
||||
}
|
||||
else {
|
||||
$translatable = [
|
||||
'data' => t('N/A'),
|
||||
'class' => ['untranslatable'],
|
||||
];
|
||||
array_unshift($rows[$bundle]['data'], $translatable);
|
||||
}
|
||||
|
||||
foreach ($field_names as $field_name) {
|
||||
$field_element = &$element[$bundle]['fields'][$field_name];
|
||||
$rows[] = [
|
||||
'data' => [
|
||||
[
|
||||
'data' => \Drupal::service('renderer')->render($field_element),
|
||||
'class' => ['translatable'],
|
||||
],
|
||||
[
|
||||
'data' => [
|
||||
'#prefix' => '<label for="' . $field_element['#id'] . '">',
|
||||
'#suffix' => '</label>',
|
||||
'bundle' => [
|
||||
'#prefix' => '<span class="visually-hidden">',
|
||||
'#suffix' => '</span> ',
|
||||
'#plain_text' => $element[$bundle]['settings']['#label'],
|
||||
],
|
||||
'field' => [
|
||||
'#plain_text' => $field_element['#label'],
|
||||
],
|
||||
],
|
||||
'class' => ['field'],
|
||||
],
|
||||
[
|
||||
'data' => '',
|
||||
'class' => ['operations'],
|
||||
],
|
||||
],
|
||||
'#field_name' => $field_name,
|
||||
'class' => ['field-settings'],
|
||||
];
|
||||
|
||||
if (!empty($element[$bundle]['columns'][$field_name])) {
|
||||
$column_element = &$element[$bundle]['columns'][$field_name];
|
||||
foreach (Element::children($column_element) as $key) {
|
||||
$column_label = $column_element[$key]['#title'];
|
||||
unset($column_element[$key]['#title']);
|
||||
$rows[] = [
|
||||
'data' => [
|
||||
[
|
||||
'data' => \Drupal::service('renderer')->render($column_element[$key]),
|
||||
'class' => ['translatable'],
|
||||
],
|
||||
[
|
||||
'data' => [
|
||||
'#prefix' => '<label for="' . $column_element[$key]['#id'] . '">',
|
||||
'#suffix' => '</label>',
|
||||
'bundle' => [
|
||||
'#prefix' => '<span class="visually-hidden">',
|
||||
'#suffix' => '</span> ',
|
||||
'#plain_text' => $element[$bundle]['settings']['#label'],
|
||||
],
|
||||
'field' => [
|
||||
'#prefix' => '<span class="visually-hidden">',
|
||||
'#suffix' => '</span> ',
|
||||
'#plain_text' => $field_element['#label'],
|
||||
],
|
||||
'columns' => [
|
||||
'#plain_text' => $column_label,
|
||||
],
|
||||
],
|
||||
'class' => ['column'],
|
||||
],
|
||||
[
|
||||
'data' => '',
|
||||
'class' => ['operations'],
|
||||
],
|
||||
],
|
||||
'class' => ['column-settings'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$build['#rows'] = $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for content_translation_admin_settings_form().
|
||||
*
|
||||
* @see content_translation_admin_settings_form_submit()
|
||||
*/
|
||||
function content_translation_form_language_content_settings_validate(array $form, FormStateInterface $form_state): void {
|
||||
$settings = &$form_state->getValue('settings');
|
||||
foreach ($settings as $entity_type => $entity_settings) {
|
||||
foreach ($entity_settings as $bundle => $bundle_settings) {
|
||||
if (!empty($bundle_settings['translatable'])) {
|
||||
$name = "settings][$entity_type][$bundle][translatable";
|
||||
|
||||
$translatable_fields = isset($settings[$entity_type][$bundle]['fields']) ? array_filter($settings[$entity_type][$bundle]['fields']) : FALSE;
|
||||
if (empty($translatable_fields)) {
|
||||
$t_args = ['%bundle' => $form['settings'][$entity_type][$bundle]['settings']['#label']];
|
||||
$form_state->setErrorByName($name, t('At least one field needs to be translatable to enable %bundle for translation.', $t_args));
|
||||
}
|
||||
|
||||
$values = $bundle_settings['settings']['language'];
|
||||
if (empty($values['language_alterable']) && \Drupal::languageManager()->isLanguageLocked($values['langcode'])) {
|
||||
foreach (\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_LOCKED) as $language) {
|
||||
$locked_languages[] = $language->getName();
|
||||
}
|
||||
$form_state->setErrorByName($name, t('Translation is not supported if language is always one of: @locked_languages', ['@locked_languages' => implode(', ', $locked_languages)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for content_translation_admin_settings_form().
|
||||
*
|
||||
* @see content_translation_admin_settings_form_validate()
|
||||
*/
|
||||
function content_translation_form_language_content_settings_submit(array $form, FormStateInterface $form_state): void {
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
|
||||
$content_translation_manager = \Drupal::service('content_translation.manager');
|
||||
$entity_types = $form_state->getValue('entity_types');
|
||||
$settings = &$form_state->getValue('settings');
|
||||
|
||||
// If an entity type is not translatable all its bundles and fields must be
|
||||
// marked as non-translatable. Similarly, if a bundle is made non-translatable
|
||||
// all of its fields will be not translatable.
|
||||
foreach ($settings as $entity_type_id => &$entity_settings) {
|
||||
foreach ($entity_settings as $bundle => &$bundle_settings) {
|
||||
$fields = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle);
|
||||
if (!empty($bundle_settings['translatable'])) {
|
||||
$bundle_settings['translatable'] = $bundle_settings['translatable'] && $entity_types[$entity_type_id];
|
||||
}
|
||||
if (!empty($bundle_settings['fields'])) {
|
||||
foreach ($bundle_settings['fields'] as $field_name => $translatable) {
|
||||
$translatable = $translatable && $bundle_settings['translatable'];
|
||||
// If we have column settings and no column is translatable, no point
|
||||
// in making the field translatable.
|
||||
if (isset($bundle_settings['columns'][$field_name]) && !array_filter($bundle_settings['columns'][$field_name])) {
|
||||
$translatable = FALSE;
|
||||
}
|
||||
$field_config = $fields[$field_name]->getConfig($bundle);
|
||||
if ($field_config->isTranslatable() != $translatable) {
|
||||
$field_config
|
||||
->setTranslatable($translatable)
|
||||
->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($bundle_settings['translatable'])) {
|
||||
// Store whether a bundle has translation enabled or not.
|
||||
$content_translation_manager->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
|
||||
|
||||
// Store any other bundle settings.
|
||||
if ($content_translation_manager instanceof BundleTranslationSettingsInterface) {
|
||||
$content_translation_manager->setBundleTranslationSettings($entity_type_id, $bundle, $bundle_settings['settings']['content_translation']);
|
||||
}
|
||||
|
||||
// Save translation_sync settings.
|
||||
if (!empty($bundle_settings['columns'])) {
|
||||
foreach ($bundle_settings['columns'] as $field_name => $column_settings) {
|
||||
$field_config = $fields[$field_name]->getConfig($bundle);
|
||||
if ($field_config->isTranslatable()) {
|
||||
$field_config->setThirdPartySetting('content_translation', 'translation_sync', $column_settings);
|
||||
}
|
||||
// If the field does not have translatable enabled we need to reset
|
||||
// the sync settings to their defaults.
|
||||
else {
|
||||
$field_config->unsetThirdPartySetting('content_translation', 'translation_sync');
|
||||
}
|
||||
$field_config->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure menu router information is correctly rebuilt.
|
||||
\Drupal::service('router.builder')->setRebuildNeeded();
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
name: 'Content Translation'
|
||||
type: module
|
||||
description: 'Allows users to translate content.'
|
||||
dependencies:
|
||||
- drupal:language
|
||||
package: Multilingual
|
||||
version: VERSION
|
||||
configure: language.content_settings_page
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Installation functions for Content Translation module.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Installer\InstallerKernel;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function content_translation_install(): void {
|
||||
// Assign a fairly low weight to ensure our implementation of
|
||||
// hook_module_implements_alter() is run among the last ones.
|
||||
module_set_weight('content_translation', 10);
|
||||
|
||||
// Skip the guidance messages about enabling translation features if the
|
||||
// module was installed in the Drupal installation process.
|
||||
if (InstallerKernel::installationAttempted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Translation works when at least two languages are added.
|
||||
if (count(\Drupal::languageManager()->getLanguages()) < 2) {
|
||||
$t_args = [
|
||||
':language_url' => Url::fromRoute('entity.configurable_language.collection')->toString(),
|
||||
];
|
||||
$message = t('This site has only a single language enabled. <a href=":language_url">Add at least one more language</a> in order to translate content.', $t_args);
|
||||
\Drupal::messenger()->addWarning($message);
|
||||
}
|
||||
// Point the user to the content translation settings.
|
||||
$t_args = [
|
||||
':settings_url' => Url::fromRoute('language.content_settings_page')->toString(),
|
||||
];
|
||||
$message = t('<a href=":settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, or any other element you wish to translate.', $t_args);
|
||||
\Drupal::messenger()->addWarning($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_update_last_removed().
|
||||
*/
|
||||
function content_translation_update_last_removed(): int {
|
||||
return 8400;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
drupal.content_translation.admin:
|
||||
version: VERSION
|
||||
js:
|
||||
js/content_translation.admin.js: {}
|
||||
css:
|
||||
theme:
|
||||
css/content_translation.admin.css: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/once
|
||||
moved_files:
|
||||
content_translation/content_translation.admin.js:
|
||||
deprecation_version: drupal:11.1.0
|
||||
removed_version: drupal:12.0.0
|
||||
deprecation_link: https://www.drupal.org/node/3471539
|
||||
js:
|
||||
content_translation_admin.js: 'js/content_translation.admin.js'
|
||||
@ -0,0 +1,14 @@
|
||||
# Content Translation extension relation types.
|
||||
# See https://tools.ietf.org/html/rfc5988#section-4.2.
|
||||
drupal:content-translation-overview:
|
||||
uri: https://drupal.org/link-relations/drupal-content-translation-overview
|
||||
description: A page where translations of a resource can be viewed.
|
||||
drupal:content-translation-add:
|
||||
uri: https://drupal.org/link-relations/drupal-content-translation-add
|
||||
description: A page where a translation of a resource can be created.
|
||||
drupal:content-translation-edit:
|
||||
uri: https://drupal.org/link-relations/drupal-content-translation-edit
|
||||
description: A page where a translation of a resource can be edited.
|
||||
drupal:content-translation-delete:
|
||||
uri: https://drupal.org/link-relations/drupal-content-translation-delete
|
||||
description: A page where a translation of a resource can be deleted.
|
||||
@ -0,0 +1,3 @@
|
||||
content_translation.contextual_links:
|
||||
deriver: 'Drupal\content_translation\Plugin\Derivative\ContentTranslationContextualLinks'
|
||||
weight: 2
|
||||
@ -0,0 +1,3 @@
|
||||
content_translation.local_tasks:
|
||||
deriver: 'Drupal\content_translation\Plugin\Derivative\ContentTranslationLocalTasks'
|
||||
weight: 100
|
||||
169
web/core/modules/content_translation/content_translation.module
Normal file
169
web/core/modules/content_translation/content_translation.module
Normal file
@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
*/
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
/**
|
||||
* Installs Content Translation's fields for a given entity type.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
*
|
||||
* @todo Generalize this code in https://www.drupal.org/node/2346013.
|
||||
*/
|
||||
function _content_translation_install_field_storage_definitions($entity_type_id): void {
|
||||
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
|
||||
$field_manager = \Drupal::service('entity_field.manager');
|
||||
/** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $schema_repository */
|
||||
$schema_repository = \Drupal::service('entity.last_installed_schema.repository');
|
||||
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
|
||||
$field_manager->useCaches(FALSE);
|
||||
$storage_definitions = $field_manager->getFieldStorageDefinitions($entity_type_id);
|
||||
$field_manager->useCaches(TRUE);
|
||||
$installed_storage_definitions = $schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id);
|
||||
foreach (array_diff_key($storage_definitions, $installed_storage_definitions) as $storage_definition) {
|
||||
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition */
|
||||
if ($storage_definition->getProvider() == 'content_translation') {
|
||||
$definition_update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type_id, 'content_translation', $storage_definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access callback for the translation overview page.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity whose translation overview should be displayed.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
function content_translation_translate_access(EntityInterface $entity) {
|
||||
$account = \Drupal::currentUser();
|
||||
$condition = $entity instanceof ContentEntityInterface && $entity->access('view') &&
|
||||
!$entity->getUntranslated()->language()->isLocked() && \Drupal::languageManager()->isMultilingual() && $entity->isTranslatable() &&
|
||||
($account->hasPermission('create content translations') || $account->hasPermission('update content translations') || $account->hasPermission('delete content translations') ||
|
||||
($account->hasPermission('translate editable entities') && $entity->access('update')));
|
||||
return AccessResult::allowedIf($condition)->cachePerPermissions()->addCacheableDependency($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a widget to enable content translation per entity bundle.
|
||||
*
|
||||
* Backward compatibility layer to support entities not using the language
|
||||
* configuration form element.
|
||||
*
|
||||
* @todo Remove once all core entities have language configuration.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of the entity being configured for translation.
|
||||
* @param string $bundle
|
||||
* The bundle of the entity being configured for translation.
|
||||
* @param array $form
|
||||
* The configuration form array.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
function content_translation_enable_widget($entity_type, $bundle, array &$form, FormStateInterface $form_state) {
|
||||
$key = $form_state->get(['content_translation', 'key']);
|
||||
$context = $form_state->get(['language', $key]) ?: [];
|
||||
$context += ['entity_type' => $entity_type, 'bundle' => $bundle];
|
||||
$form_state->set(['language', $key], $context);
|
||||
$element = content_translation_language_configuration_element_process(['#name' => $key], $form_state, $form);
|
||||
unset($element['content_translation']['#element_validate']);
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process callback: Expands the language_configuration form element.
|
||||
*
|
||||
* @param array $element
|
||||
* Form API element.
|
||||
*
|
||||
* @return array
|
||||
* Processed language configuration element.
|
||||
*/
|
||||
function content_translation_language_configuration_element_process(array $element, FormStateInterface $form_state, array &$form) {
|
||||
if (empty($element['#content_translation_skip_alter']) && \Drupal::currentUser()->hasPermission('administer content translation')) {
|
||||
$key = $element['#name'];
|
||||
$form_state->set(['content_translation', 'key'], $key);
|
||||
$context = $form_state->get(['language', $key]);
|
||||
|
||||
$element['content_translation'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Enable translation'),
|
||||
// For new bundle, we don't know the bundle name yet,
|
||||
// default to no translatability.
|
||||
'#default_value' => $context['bundle'] ? \Drupal::service('content_translation.manager')->isEnabled($context['entity_type'], $context['bundle']) : FALSE,
|
||||
'#element_validate' => ['content_translation_language_configuration_element_validate'],
|
||||
];
|
||||
|
||||
$submit_name = isset($form['actions']['save_continue']) ? 'save_continue' : 'submit';
|
||||
// Only add the submit handler on the submit button if the #submit property
|
||||
// is already available, otherwise this breaks the form submit function.
|
||||
if (isset($form['actions'][$submit_name]['#submit'])) {
|
||||
$form['actions'][$submit_name]['#submit'][] = 'content_translation_language_configuration_element_submit';
|
||||
}
|
||||
else {
|
||||
$form['#submit'][] = 'content_translation_language_configuration_element_submit';
|
||||
}
|
||||
}
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for the language_configuration form element.
|
||||
*
|
||||
* Checks whether translation can be enabled: if language is set to one of the
|
||||
* special languages and language selector is not hidden, translation cannot be
|
||||
* enabled.
|
||||
*
|
||||
* @see content_translation_language_configuration_element_submit()
|
||||
*/
|
||||
function content_translation_language_configuration_element_validate($element, FormStateInterface $form_state, array $form): void {
|
||||
$key = $form_state->get(['content_translation', 'key']);
|
||||
$values = $form_state->getValue($key);
|
||||
if (!$values['language_alterable'] && $values['content_translation'] && \Drupal::languageManager()->isLanguageLocked($values['langcode'])) {
|
||||
foreach (\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_LOCKED) as $language) {
|
||||
$locked_languages[$language->getId()] = $language->getName();
|
||||
}
|
||||
// @todo Set the correct form element name as soon as the element parents
|
||||
// are correctly set. We should be using NestedArray::getValue() but for
|
||||
// now we cannot.
|
||||
$form_state->setErrorByName('', t('"Show language selector" is not compatible with translating content that has default language: %choice. Either do not hide the language selector or pick a specific language.', ['%choice' => $locked_languages[$values['langcode']]]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for element.
|
||||
*
|
||||
* Stores the content translation settings.
|
||||
*
|
||||
* @see content_translation_language_configuration_element_validate()
|
||||
*/
|
||||
function content_translation_language_configuration_element_submit(array $form, FormStateInterface $form_state): void {
|
||||
$key = $form_state->get(['content_translation', 'key']);
|
||||
$context = $form_state->get(['language', $key]);
|
||||
$enabled = $form_state->getValue([$key, 'content_translation']);
|
||||
|
||||
if (\Drupal::service('content_translation.manager')->isEnabled($context['entity_type'], $context['bundle']) != $enabled) {
|
||||
\Drupal::service('content_translation.manager')->setEnabled($context['entity_type'], $context['bundle'], $enabled);
|
||||
\Drupal::service('router.builder')->setRebuildNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig.
|
||||
*/
|
||||
function content_translation_preprocess_language_content_settings_table(&$variables): void {
|
||||
\Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin');
|
||||
_content_translation_preprocess_language_content_settings_table($variables);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
administer content translation:
|
||||
title: 'Administer translation settings'
|
||||
create content translations:
|
||||
title: 'Create translations'
|
||||
update content translations:
|
||||
title: 'Edit translations'
|
||||
delete content translations:
|
||||
title: 'Delete translations'
|
||||
translate any entity:
|
||||
title: 'Translate any entity'
|
||||
translate editable entities:
|
||||
title: 'Manage translations for any entity that the user can edit'
|
||||
|
||||
permission_callbacks:
|
||||
- \Drupal\content_translation\ContentTranslationPermissions::contentPermissions
|
||||
@ -0,0 +1,42 @@
|
||||
parameters:
|
||||
content_translation.moved_classes:
|
||||
'Drupal\content_translation\Plugin\migrate\source\I18nQueryTrait':
|
||||
class: 'Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait'
|
||||
deprecation_version: drupal:11.2.0
|
||||
removed_version: drupal:12.0.0
|
||||
change_record: https://www.drupal.org/node/3439256
|
||||
services:
|
||||
_defaults:
|
||||
autoconfigure: true
|
||||
content_translation.synchronizer:
|
||||
class: Drupal\content_translation\FieldTranslationSynchronizer
|
||||
arguments: ['@entity_type.manager', '@plugin.manager.field.field_type']
|
||||
Drupal\content_translation\FieldTranslationSynchronizerInterface: '@content_translation.synchronizer'
|
||||
|
||||
content_translation.subscriber:
|
||||
class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber
|
||||
arguments: ['@content_translation.manager']
|
||||
|
||||
content_translation.delete_access:
|
||||
class: Drupal\content_translation\Access\ContentTranslationDeleteAccess
|
||||
arguments: ['@entity_type.manager', '@content_translation.manager']
|
||||
tags:
|
||||
- { name: access_check, applies_to: _access_content_translation_delete }
|
||||
|
||||
content_translation.overview_access:
|
||||
class: Drupal\content_translation\Access\ContentTranslationOverviewAccess
|
||||
arguments: ['@entity_type.manager']
|
||||
tags:
|
||||
- { name: access_check, applies_to: _access_content_translation_overview }
|
||||
|
||||
content_translation.manage_access:
|
||||
class: Drupal\content_translation\Access\ContentTranslationManageAccessCheck
|
||||
arguments: ['@entity_type.manager', '@language_manager']
|
||||
tags:
|
||||
- { name: access_check, applies_to: _access_content_translation_manage }
|
||||
|
||||
content_translation.manager:
|
||||
class: Drupal\content_translation\ContentTranslationManager
|
||||
arguments: ['@entity_type.manager', '@entity_type.bundle.info']
|
||||
Drupal\content_translation\ContentTranslationManagerInterface: '@content_translation.manager'
|
||||
Drupal\content_translation\BundleTranslationSettingsInterface: '@content_translation.manager'
|
||||
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @file
|
||||
* Styles for the content language administration page.
|
||||
*/
|
||||
|
||||
.language-content-settings-form .bundle {
|
||||
width: 24%;
|
||||
}
|
||||
.language-content-settings-form .field {
|
||||
width: 24%;
|
||||
padding-left: 3em; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .language-content-settings-form .field {
|
||||
padding-right: 3em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
.language-content-settings-form .column {
|
||||
padding-left: 5em; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .language-content-settings-form .column {
|
||||
padding-right: 5em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
.language-content-settings-form .field label,
|
||||
.language-content-settings-form .column label {
|
||||
font-weight: normal;
|
||||
}
|
||||
.language-content-settings-form .translatable {
|
||||
width: 1%;
|
||||
}
|
||||
.language-content-settings-form .operations {
|
||||
width: 75%;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
---
|
||||
label: 'Configuring content language settings and translation'
|
||||
related:
|
||||
- core.translations
|
||||
- language.add
|
||||
---
|
||||
{% set translation_settings_link_text %}{% trans %}Content language and translation{% endtrans %}{% endset %}
|
||||
{% set translation_settings_link = render_var(help_route_link(translation_settings_link_text, 'language.content_settings_page')) %}
|
||||
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
|
||||
<h2>{% trans %}Goal{% endtrans %}</h2>
|
||||
<p>{% trans %}Configure language and translation settings for one or more content entity types (see {{ content_structure_topic }} for an overview of content entities). To do this, you must have at least two languages configured. Afterwards, you will have a <em>Translate</em> operation available for your content entities, either as a tab or link when you are viewing or editing content, or on content administration pages.{% endtrans %}</p>
|
||||
<h2>{% trans %}Steps{% endtrans %}</h2>
|
||||
<ol>
|
||||
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> > <em>Region and language</em> > <em>{{ translation_settings_link }}</em>.{% endtrans %}</li>
|
||||
<li>{% trans %}Under <em>Custom language settings</em>, find the content entity types that should have customized language settings on your site. Check the box next to each one. A section will appear below the list with settings for that entity type.{% endtrans %}</li>
|
||||
<li>{% trans %}For each entity type you checked, in the settings section below check the boxes for each entity sub-type that should be <em>Translatable</em> on your site. If the entity type does not have sub-types, there is just one check box for the entity type as a whole.{% endtrans %}</li>
|
||||
<li>{% trans %}For each entity type or subtype, select the <em>Default language</em>. Also, if you want to have languages other than the default available when you create content, check <em>Show language selector on create and edit pages</em>.{% endtrans %}</li>
|
||||
<li>{% trans %}For each <em>Translatable</em> type or sub-type, look through the list of fields for the type or sub-type, and verify that only the fields that should be translatable are checked. For example, you would probably want to translate a <em>Title</em> field, but you might not want to translate a <em>Start date</em> field.{% endtrans %}</li>
|
||||
<li>{% trans %}Click <em>Save configuration</em> when all of your changes are complete.{% endtrans %}</li>
|
||||
</ol>
|
||||
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @file
|
||||
* Content Translation admin behaviors.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Forces applicable options to be checked as translatable.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches content translation dependent options to the UI.
|
||||
*/
|
||||
Drupal.behaviors.contentTranslationDependentOptions = {
|
||||
attach(context) {
|
||||
const $context = $(context);
|
||||
const options = drupalSettings.contentTranslationDependentOptions;
|
||||
let $fields;
|
||||
|
||||
function fieldsChangeHandler($fields, dependentColumns) {
|
||||
return function (e) {
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check(
|
||||
$fields,
|
||||
dependentColumns,
|
||||
$(e.target),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// We're given a generic name to look for so we find all inputs containing
|
||||
// that name and copy over the input values that require all columns to be
|
||||
// translatable.
|
||||
if (options?.dependent_selectors) {
|
||||
Object.keys(options.dependent_selectors).forEach((field) => {
|
||||
$fields = $context.find(`input[name^="${field}"]`);
|
||||
const dependentColumns = options.dependent_selectors[field];
|
||||
|
||||
$fields.on('change', fieldsChangeHandler($fields, dependentColumns));
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check(
|
||||
$fields,
|
||||
dependentColumns,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
check($fields, dependentColumns, $changed) {
|
||||
let $element = $changed;
|
||||
let column;
|
||||
|
||||
function filterFieldsList(index, field) {
|
||||
return field.value === column;
|
||||
}
|
||||
|
||||
// A field that has many different translatable parts can also define one
|
||||
// or more columns that require all columns to be translatable.
|
||||
Object.keys(dependentColumns || {}).forEach((index) => {
|
||||
column = dependentColumns[index];
|
||||
|
||||
if (!$changed) {
|
||||
$element = $fields.filter(filterFieldsList);
|
||||
}
|
||||
|
||||
if (
|
||||
$element.length &&
|
||||
$element[0].matches(`input[value="${column}"]:checked`)
|
||||
) {
|
||||
$fields.prop('checked', true).not($element).prop('disabled', true);
|
||||
} else {
|
||||
$fields.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes field translatability inherit bundle translatability.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches content translation behavior.
|
||||
*/
|
||||
Drupal.behaviors.contentTranslation = {
|
||||
attach(context) {
|
||||
// Initially hide all field rows for non translatable bundles and all
|
||||
// column rows for non translatable fields.
|
||||
once(
|
||||
'translation-entity-admin-hide',
|
||||
// Keep jQuery because of the use of `:input`.
|
||||
$(context).find('table .bundle-settings .translatable :input'),
|
||||
).forEach((input) => {
|
||||
const $input = $(input);
|
||||
const $bundleSettings = $input.closest('.bundle-settings');
|
||||
if (!input.checked) {
|
||||
$bundleSettings.nextUntil('.bundle-settings').hide();
|
||||
} else {
|
||||
$bundleSettings
|
||||
.nextUntil('.bundle-settings', '.field-settings')
|
||||
.find('.translatable :input:not(:checked)')
|
||||
.closest('.field-settings')
|
||||
.nextUntil(':not(.column-settings)')
|
||||
.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// When a bundle is made translatable all of its fields should inherit
|
||||
// this setting. Instead when it is made non translatable its fields are
|
||||
// hidden, since their translatability no longer matters.
|
||||
$(once('translation-entity-admin-bind', 'body'))
|
||||
.on('click', 'table .bundle-settings .translatable :input', (e) => {
|
||||
const $target = $(e.target);
|
||||
const $bundleSettings = $target.closest('.bundle-settings');
|
||||
const $settings = $bundleSettings.nextUntil('.bundle-settings');
|
||||
const $fieldSettings = $settings.filter('.field-settings');
|
||||
if (e.target.checked) {
|
||||
$bundleSettings
|
||||
.find('.operations :input[name$="[language_alterable]"]')
|
||||
.prop('checked', true);
|
||||
$fieldSettings.find('.translatable :input').prop('checked', true);
|
||||
$settings.show();
|
||||
} else {
|
||||
$settings.hide();
|
||||
}
|
||||
})
|
||||
.on('click', 'table .field-settings .translatable :input', (e) => {
|
||||
const $target = $(e.target);
|
||||
const $fieldSettings = $target.closest('.field-settings');
|
||||
const $columnSettings = $fieldSettings.nextUntil(
|
||||
'.field-settings, .bundle-settings',
|
||||
);
|
||||
if (e.target.checked) {
|
||||
$columnSettings.show();
|
||||
} else {
|
||||
$columnSettings.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
@ -0,0 +1,50 @@
|
||||
id: d6_custom_block_translation
|
||||
label: Content block translations
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d6_box_translation
|
||||
process:
|
||||
id:
|
||||
plugin: migration_lookup
|
||||
migration: d6_custom_block
|
||||
source:
|
||||
- bid
|
||||
langcode: language
|
||||
info:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- title_translated
|
||||
- title
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
'body/value':
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- body_translated
|
||||
- body
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
'body/format':
|
||||
plugin: migration_lookup
|
||||
migration: d6_filter_format
|
||||
source: format
|
||||
destination:
|
||||
plugin: entity:block_content
|
||||
no_stub: true
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d6_filter_format
|
||||
- block_content_body_field
|
||||
- d6_custom_block
|
||||
- language
|
||||
@ -0,0 +1,30 @@
|
||||
id: d6_entity_reference_translation
|
||||
label: Entity reference translations
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Multilingual
|
||||
- Follow-up migration
|
||||
deriver: Drupal\migrate_drupal\Plugin\migrate\EntityReferenceTranslationDeriver
|
||||
provider:
|
||||
- content_translation
|
||||
- migrate_drupal
|
||||
# Supported target types for entity reference translation migrations. The array
|
||||
# keys are the supported target types and the values are arrays of migrations
|
||||
# to lookup for the translated entity IDs.
|
||||
target_types:
|
||||
node:
|
||||
- d6_node_translation
|
||||
- d6_node_complete
|
||||
# The source plugin will be set by the deriver.
|
||||
source:
|
||||
plugin: empty
|
||||
key: default
|
||||
target: default
|
||||
# The process pipeline will be set by the deriver.
|
||||
process: []
|
||||
# The destination plugin will be set by the deriver.
|
||||
destination:
|
||||
plugin: null
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
@ -0,0 +1,63 @@
|
||||
# cspell:ignore newnode
|
||||
id: d6_language_content_comment_settings
|
||||
label: Drupal 6 language content comment settings
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Configuration
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d6_language_content_settings
|
||||
constants:
|
||||
target_type: comment
|
||||
id_prefix: comment_node_
|
||||
label_suffix: comment
|
||||
process:
|
||||
# Ignore i18n_node_options_[node_type] options not available in Drupal 8,
|
||||
# i18n_required_node and i18n_newnode_current
|
||||
target_bundle:
|
||||
-
|
||||
plugin: concat
|
||||
source:
|
||||
- constants/id_prefix
|
||||
- type
|
||||
-
|
||||
plugin: static_map
|
||||
bypass: true
|
||||
# The Forum module provides its own comment type (comment_forum), which we
|
||||
# want to reuse if it exists.
|
||||
map:
|
||||
comment_node_forum: comment_forum
|
||||
target_entity_type_id: constants/target_type
|
||||
default_langcode:
|
||||
-
|
||||
plugin: static_map
|
||||
source: language_content_type
|
||||
map:
|
||||
0: site_default
|
||||
1: current_interface
|
||||
2: current_interface
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
language_alterable:
|
||||
plugin: static_map
|
||||
source: language_content_type
|
||||
map:
|
||||
0: false
|
||||
1: true
|
||||
2: true
|
||||
'third_party_settings/content_translation/enabled':
|
||||
plugin: static_map
|
||||
source: language_content_type
|
||||
map:
|
||||
# In the case of being 0, it will be skipped. We are not actually setting
|
||||
# a null value.
|
||||
0: NULL
|
||||
1: false
|
||||
2: false
|
||||
destination:
|
||||
plugin: entity:language_content_settings
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d6_comment_type
|
||||
@ -0,0 +1,70 @@
|
||||
# cspell:ignore mlid plid
|
||||
id: d6_menu_links_localized
|
||||
label: Menu links localized
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: menu_link
|
||||
constants:
|
||||
bundle: menu_link_content
|
||||
process:
|
||||
skip_not_localized:
|
||||
plugin: skip_on_empty
|
||||
source: is_localized
|
||||
method: row
|
||||
id: mlid
|
||||
title: link_title
|
||||
description: description
|
||||
langcode: language
|
||||
menu_name:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
# The menu migration is in the system module.
|
||||
migration: d6_menu
|
||||
source: menu_name
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
-
|
||||
plugin: static_map
|
||||
map:
|
||||
management: admin
|
||||
bypass: true
|
||||
'link/uri':
|
||||
plugin: link_uri
|
||||
source: link_path
|
||||
'link/options':
|
||||
plugin: link_options
|
||||
source: options
|
||||
route:
|
||||
plugin: route
|
||||
source:
|
||||
- link_path
|
||||
- options
|
||||
route_name: '@route/route_name'
|
||||
route_parameters: '@route/route_parameters'
|
||||
url: '@route/url'
|
||||
options: '@route/options'
|
||||
external: external
|
||||
weight: weight
|
||||
expanded: expanded
|
||||
enabled: enabled
|
||||
parent:
|
||||
plugin: menu_link_parent
|
||||
source:
|
||||
- plid
|
||||
- '@menu_name'
|
||||
- parent_link_path
|
||||
changed: updated
|
||||
destination:
|
||||
plugin: entity:menu_link_content
|
||||
no_stub: true
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d6_language_content_menu_settings
|
||||
- d6_menu_links
|
||||
@ -0,0 +1,56 @@
|
||||
# cspell:ignore mlid
|
||||
id: d6_menu_links_translation
|
||||
label: Menu links
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d6_menu_link_translation
|
||||
process:
|
||||
id: mlid
|
||||
langcode: language
|
||||
title:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- title_translated
|
||||
- link_title
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
description:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- description_translated
|
||||
- description
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
menu_name:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
# The menu migration is in the system module.
|
||||
migration: d6_menu
|
||||
source: menu_name
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
-
|
||||
plugin: static_map
|
||||
map:
|
||||
management: admin
|
||||
bypass: true
|
||||
destination:
|
||||
plugin: entity:menu_link_content
|
||||
default_bundle: menu_link_content
|
||||
no_stub: true
|
||||
translations: true
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d6_menu
|
||||
- d6_menu_links
|
||||
@ -0,0 +1,60 @@
|
||||
# cspell:ignore tnid
|
||||
id: d6_node_translation
|
||||
label: Node translations
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- translation
|
||||
- Content
|
||||
- Multilingual
|
||||
class: Drupal\node\Plugin\migrate\D6NodeTranslation
|
||||
deriver: Drupal\node\Plugin\migrate\D6NodeDeriver
|
||||
source:
|
||||
plugin: d6_node
|
||||
translations: true
|
||||
process:
|
||||
# If you are using this file to build a custom migration consider removing
|
||||
# the nid field to allow incremental migrations.
|
||||
nid: tnid
|
||||
type: type
|
||||
langcode:
|
||||
plugin: default_value
|
||||
source: language
|
||||
default_value: "und"
|
||||
title: title
|
||||
uid: node_uid
|
||||
status: status
|
||||
created: created
|
||||
changed: changed
|
||||
promote: promote
|
||||
sticky: sticky
|
||||
'body/format':
|
||||
plugin: migration_lookup
|
||||
migration: d6_filter_format
|
||||
source: format
|
||||
'body/value': body
|
||||
'body/summary': teaser
|
||||
revision_uid: revision_uid
|
||||
revision_log: log
|
||||
revision_timestamp: timestamp
|
||||
content_translation_source: source_langcode
|
||||
|
||||
# unmapped d6 fields.
|
||||
# translate
|
||||
# moderate
|
||||
# comment
|
||||
|
||||
destination:
|
||||
plugin: entity:node
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d6_user
|
||||
- d6_node_type
|
||||
- d6_node_settings
|
||||
- d6_filter_format
|
||||
- language
|
||||
optional:
|
||||
- d6_field_instance_widget_settings
|
||||
- d6_field_formatter_settings
|
||||
- d6_upload_field_instance
|
||||
@ -0,0 +1,45 @@
|
||||
id: d6_taxonomy_term_localized_translation
|
||||
label: Taxonomy localized term translations
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d6_term_localized_translation
|
||||
translations: true
|
||||
process:
|
||||
# If you are using this file to build a custom migration consider removing
|
||||
# the tid field to allow incremental migrations.
|
||||
tid: tid
|
||||
langcode: language
|
||||
vid:
|
||||
plugin: migration_lookup
|
||||
migration: d6_taxonomy_vocabulary
|
||||
source: vid
|
||||
name:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- name_translated
|
||||
- name
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
description:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- description_translated
|
||||
- description
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
destination:
|
||||
plugin: entity:taxonomy_term
|
||||
translations: true
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d6_taxonomy_term
|
||||
- language
|
||||
@ -0,0 +1,43 @@
|
||||
id: d6_taxonomy_term_translation
|
||||
label: Taxonomy terms
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d6_taxonomy_term
|
||||
translations: true
|
||||
process:
|
||||
# If you are using this file to build a custom migration consider removing
|
||||
# the tid field to allow incremental migrations.
|
||||
tid: tid
|
||||
langcode: language
|
||||
vid:
|
||||
plugin: migration_lookup
|
||||
migration: d6_taxonomy_vocabulary
|
||||
source: vid
|
||||
name: name
|
||||
description: description
|
||||
weight: weight
|
||||
# Only attempt to stub real (non-zero) parents.
|
||||
parent_id:
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: process
|
||||
source: parent
|
||||
-
|
||||
plugin: migration_lookup
|
||||
migration: d6_taxonomy_term
|
||||
parent:
|
||||
plugin: default_value
|
||||
default_value: 0
|
||||
source: '@parent_id'
|
||||
changed: timestamp
|
||||
destination:
|
||||
plugin: entity:taxonomy_term
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d6_taxonomy_vocabulary
|
||||
- d6_taxonomy_term
|
||||
- language
|
||||
@ -0,0 +1,50 @@
|
||||
id: d6_term_node_translation
|
||||
label: Term/node translation relationships
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Content
|
||||
- Multilingual
|
||||
deriver: Drupal\taxonomy\Plugin\migrate\D6TermNodeDeriver
|
||||
source:
|
||||
plugin: d6_term_node
|
||||
process:
|
||||
dest_nid:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
migration:
|
||||
- d6_node_complete
|
||||
- d6_node_translation
|
||||
source: nid
|
||||
-
|
||||
plugin: node_complete_node_translation_lookup
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
nid:
|
||||
-
|
||||
plugin: extract
|
||||
index: [0]
|
||||
source: '@dest_nid'
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
langcode:
|
||||
-
|
||||
plugin: extract
|
||||
index: [1]
|
||||
source: '@dest_nid'
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
type: type
|
||||
# The actual field name is dynamic and will be added by the builder.
|
||||
destination:
|
||||
plugin: entity:node
|
||||
translations: true
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d6_vocabulary_entity_display
|
||||
- d6_vocabulary_entity_form_display
|
||||
- d6_node
|
||||
- d6_node_translation
|
||||
- language
|
||||
@ -0,0 +1,28 @@
|
||||
id: d7_comment_entity_translation
|
||||
label: Comment entity translations
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- translation
|
||||
- Content
|
||||
class: Drupal\comment\Plugin\migrate\D7Comment
|
||||
source:
|
||||
plugin: d7_comment_entity_translation
|
||||
process:
|
||||
cid: entity_id
|
||||
subject: subject
|
||||
langcode: language
|
||||
uid: uid
|
||||
status: status
|
||||
created: created
|
||||
changed: changed
|
||||
content_translation_source: source
|
||||
content_translation_outdated: translate
|
||||
destination:
|
||||
plugin: entity:comment
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_entity_translation_settings
|
||||
- d7_comment
|
||||
@ -0,0 +1,50 @@
|
||||
id: d7_custom_block_translation
|
||||
label: Content block translations
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d7_block_custom_translation
|
||||
process:
|
||||
id:
|
||||
plugin: migration_lookup
|
||||
migration: d7_custom_block
|
||||
source:
|
||||
- bid
|
||||
langcode: language
|
||||
info:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- title_translated
|
||||
- title
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
'body/value':
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- body_translated
|
||||
- body
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
'body/format':
|
||||
plugin: migration_lookup
|
||||
migration: d7_filter_format
|
||||
source: format
|
||||
destination:
|
||||
plugin: entity:block_content
|
||||
no_stub: true
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d7_filter_format
|
||||
- block_content_body_field
|
||||
- d7_custom_block
|
||||
- language
|
||||
@ -0,0 +1,30 @@
|
||||
id: d7_entity_reference_translation
|
||||
label: Entity reference translations
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Multilingual
|
||||
- Follow-up migration
|
||||
deriver: Drupal\migrate_drupal\Plugin\migrate\EntityReferenceTranslationDeriver
|
||||
provider:
|
||||
- content_translation
|
||||
- migrate_drupal
|
||||
# Supported target types for entity reference translation migrations. The array
|
||||
# keys are the supported target types and the values are arrays of migrations
|
||||
# to lookup for the translated entity IDs.
|
||||
target_types:
|
||||
node:
|
||||
- d7_node_translation
|
||||
- d7_node_complete
|
||||
# The source plugin will be set by the deriver.
|
||||
source:
|
||||
plugin: empty
|
||||
key: default
|
||||
target: default
|
||||
# The process pipeline will be set by the deriver.
|
||||
process: []
|
||||
# The destination plugin will be set by the deriver.
|
||||
destination:
|
||||
plugin: null
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
@ -0,0 +1,34 @@
|
||||
id: d7_entity_translation_settings
|
||||
label: Drupal 7 Entity Translation settings
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Configuration
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d7_entity_translation_settings
|
||||
process:
|
||||
id: id
|
||||
target_entity_type_id: target_entity_type_id
|
||||
target_bundle: target_bundle
|
||||
default_langcode:
|
||||
plugin: static_map
|
||||
source: default_langcode
|
||||
bypass: true
|
||||
map:
|
||||
xx-et-default: site_default
|
||||
xx-et-current: current_interface
|
||||
xx-et-author: authors_default
|
||||
language_alterable: language_alterable
|
||||
third_party_settings/content_translation/enabled:
|
||||
plugin: default_value
|
||||
default_value: true
|
||||
third_party_settings/content_translation/bundle_settings/untranslatable_fields_hide: untranslatable_fields_hide
|
||||
destination:
|
||||
plugin: entity:language_content_settings
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
optional:
|
||||
- d7_comment_type
|
||||
- d7_node_type
|
||||
- d7_taxonomy_vocabulary
|
||||
@ -0,0 +1,55 @@
|
||||
id: d7_language_content_comment_settings
|
||||
label: Drupal 7 language content comment settings
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Configuration
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d7_language_content_settings
|
||||
constants:
|
||||
target_type: comment
|
||||
process:
|
||||
target_bundle:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
migration: d7_comment_type
|
||||
no_stub: true
|
||||
source:
|
||||
- type
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
target_entity_type_id: constants/target_type
|
||||
default_langcode:
|
||||
-
|
||||
plugin: static_map
|
||||
source: language_content_type
|
||||
map:
|
||||
0: site_default
|
||||
1: current_interface
|
||||
2: current_interface
|
||||
4: current_interface
|
||||
default_value: NULL
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
language_alterable:
|
||||
plugin: static_map
|
||||
source: language_content_type
|
||||
map:
|
||||
0: false
|
||||
1: true
|
||||
2: true
|
||||
4: true
|
||||
third_party_settings/content_translation/enabled:
|
||||
plugin: content_translation_enabled_setting
|
||||
source:
|
||||
- language_content_type
|
||||
- entity_translation_entity_types
|
||||
- constants/target_type
|
||||
destination:
|
||||
plugin: entity:language_content_settings
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_comment_type
|
||||
@ -0,0 +1,46 @@
|
||||
# cspell:ignore mlid
|
||||
id: d7_menu_links_localized
|
||||
label: Menu links
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d7_menu_link_localized
|
||||
constants:
|
||||
bundle: menu_link_content
|
||||
process:
|
||||
skip_translation:
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
source: skip_source_translation
|
||||
exists:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
migration: d7_menu_links
|
||||
source: mlid
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
id: mlid
|
||||
langcode: language
|
||||
title: link_title
|
||||
description: description
|
||||
menu_name:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
migration: d7_menu
|
||||
source: menu_name
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
destination:
|
||||
plugin: entity:menu_link_content
|
||||
no_stub: true
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_language_content_menu_settings
|
||||
- d7_menu_links
|
||||
@ -0,0 +1,51 @@
|
||||
# cspell:ignore mlid
|
||||
id: d7_menu_links_translation
|
||||
label: Menu links
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d7_menu_link_translation
|
||||
process:
|
||||
exists:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
migration: d7_menu_links
|
||||
source: mlid
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
id: mlid
|
||||
# Use the language from the locales_target table.
|
||||
langcode: language
|
||||
title:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- title_translated
|
||||
- link_title
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
description:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- description_translated
|
||||
- description
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
destination:
|
||||
plugin: entity:menu_link_content
|
||||
default_bundle: menu_link_content
|
||||
no_stub: true
|
||||
translations: true
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_language_content_menu_settings
|
||||
- d7_menu_links
|
||||
@ -0,0 +1,36 @@
|
||||
id: d7_node_entity_translation
|
||||
label: Node entity translations
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- translation
|
||||
- Content
|
||||
- Multilingual
|
||||
deriver: Drupal\node\Plugin\migrate\D7NodeDeriver
|
||||
source:
|
||||
plugin: d7_node_entity_translation
|
||||
process:
|
||||
nid: entity_id
|
||||
type: type
|
||||
langcode: language
|
||||
title: title
|
||||
uid: uid
|
||||
status: status
|
||||
created: created
|
||||
changed: changed
|
||||
promote: promote
|
||||
sticky: sticky
|
||||
revision_uid: revision_uid
|
||||
revision_log: log
|
||||
revision_timestamp: timestamp
|
||||
content_translation_source: source
|
||||
# Boolean indicating whether this translation needs to be updated.
|
||||
content_translation_outdated: translate
|
||||
destination:
|
||||
plugin: entity:node
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_entity_translation_settings
|
||||
- d7_node
|
||||
@ -0,0 +1,44 @@
|
||||
# cspell:ignore tnid
|
||||
id: d7_node_translation
|
||||
label: Node translations
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- translation
|
||||
- Content
|
||||
- Multilingual
|
||||
class: Drupal\node\Plugin\migrate\D7NodeTranslation
|
||||
deriver: Drupal\node\Plugin\migrate\D7NodeDeriver
|
||||
source:
|
||||
plugin: d7_node
|
||||
translations: true
|
||||
process:
|
||||
# If you are using this file to build a custom migration consider removing
|
||||
# the nid field to allow incremental migrations.
|
||||
nid: tnid
|
||||
type: type
|
||||
langcode:
|
||||
plugin: default_value
|
||||
source: language
|
||||
default_value: "und"
|
||||
title: title
|
||||
uid: node_uid
|
||||
status: status
|
||||
created: created
|
||||
changed: changed
|
||||
promote: promote
|
||||
sticky: sticky
|
||||
revision_uid: revision_uid
|
||||
revision_log: log
|
||||
revision_timestamp: timestamp
|
||||
content_translation_source: source_langcode
|
||||
destination:
|
||||
plugin: entity:node
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- d7_user
|
||||
- d7_node_type
|
||||
- language
|
||||
optional:
|
||||
- d7_field_instance
|
||||
@ -0,0 +1,33 @@
|
||||
id: d7_taxonomy_term_entity_translation
|
||||
label: Taxonomy term entity translations
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- translation
|
||||
- Content
|
||||
- Multilingual
|
||||
deriver: Drupal\taxonomy\Plugin\migrate\D7TaxonomyTermDeriver
|
||||
source:
|
||||
plugin: d7_taxonomy_term_entity_translation
|
||||
process:
|
||||
tid: entity_id
|
||||
name: name
|
||||
description/value: description
|
||||
description/format: format
|
||||
langcode: language
|
||||
status: status
|
||||
content_translation_source: source
|
||||
content_translation_outdated: translate
|
||||
content_translation_uid: uid
|
||||
content_translation_created: created
|
||||
changed: changed
|
||||
destination:
|
||||
plugin: entity:taxonomy_term
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_entity_translation_settings
|
||||
- d7_taxonomy_term
|
||||
optional:
|
||||
- d7_language_content_taxonomy_vocabulary_settings
|
||||
@ -0,0 +1,49 @@
|
||||
# cspell:ignore ltlanguage
|
||||
id: d7_taxonomy_term_localized_translation
|
||||
label: Taxonomy localized term translations
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d7_term_localized_translation
|
||||
translations: true
|
||||
process:
|
||||
# If you are using this file to build a custom migration consider removing
|
||||
# the tid field to allow incremental migrations.
|
||||
tid: tid
|
||||
# Use the language from the locales_target table.
|
||||
langcode: ltlanguage
|
||||
vid:
|
||||
plugin: migration_lookup
|
||||
migration: d7_taxonomy_vocabulary
|
||||
source: vid
|
||||
name:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- name_translated
|
||||
- name
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
description:
|
||||
-
|
||||
plugin: callback
|
||||
source:
|
||||
- description_translated
|
||||
- description
|
||||
callable: array_filter
|
||||
-
|
||||
plugin: callback
|
||||
callable: current
|
||||
destination:
|
||||
plugin: entity:taxonomy_term
|
||||
translations: true
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_taxonomy_term
|
||||
optional:
|
||||
- d7_language_content_taxonomy_vocabulary_settings
|
||||
@ -0,0 +1,46 @@
|
||||
id: d7_taxonomy_term_translation
|
||||
label: Taxonomy terms
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: d7_taxonomy_term_translation
|
||||
translations: true
|
||||
process:
|
||||
# If you are using this file to build a custom migration consider removing
|
||||
# the tid field to allow incremental migrations.
|
||||
tid: tid
|
||||
langcode: language
|
||||
vid:
|
||||
plugin: migration_lookup
|
||||
migration: d7_taxonomy_vocabulary
|
||||
source: vid
|
||||
name: name
|
||||
'description/value': description
|
||||
'description/format': format
|
||||
weight: weight
|
||||
# Only attempt to stub real (non-zero) parents.
|
||||
parent_id:
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: process
|
||||
source: parent
|
||||
-
|
||||
plugin: migration_lookup
|
||||
migration: d7_taxonomy_term
|
||||
parent:
|
||||
plugin: default_value
|
||||
default_value: 0
|
||||
source: '@parent_id'
|
||||
changed: timestamp
|
||||
destination:
|
||||
plugin: entity:taxonomy_term
|
||||
destination_module: content_translation
|
||||
translations: true
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_taxonomy_term
|
||||
optional:
|
||||
- d7_language_content_taxonomy_vocabulary_settings
|
||||
@ -0,0 +1,27 @@
|
||||
id: d7_user_entity_translation
|
||||
label: User accounts entity translations
|
||||
migration_tags:
|
||||
- Drupal 7
|
||||
- translation
|
||||
- Content
|
||||
- Multilingual
|
||||
class: Drupal\user\Plugin\migrate\User
|
||||
source:
|
||||
plugin: d7_user_entity_translation
|
||||
process:
|
||||
uid: entity_id
|
||||
langcode: language
|
||||
content_translation_source: source
|
||||
content_translation_uid: uid
|
||||
content_translation_status: status
|
||||
content_translation_outdated: translate
|
||||
content_translation_created: created
|
||||
destination:
|
||||
plugin: entity:user
|
||||
translations: true
|
||||
destination_module: content_translation
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
- d7_entity_translation_settings
|
||||
- d7_user
|
||||
@ -0,0 +1,126 @@
|
||||
# cspell:ignore mlid plid
|
||||
id: node_translation_menu_links
|
||||
label: Node Translations Menu links
|
||||
audit: true
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Drupal 7
|
||||
- Content
|
||||
- Multilingual
|
||||
source:
|
||||
plugin: menu_link
|
||||
constants:
|
||||
entity_prefix: 'entity:'
|
||||
node_prefix: 'node/'
|
||||
process:
|
||||
id: mlid
|
||||
title: link_title
|
||||
description: description
|
||||
menu_name:
|
||||
-
|
||||
plugin: migration_lookup
|
||||
# The menu migration is in the system module.
|
||||
migration:
|
||||
- d6_menu
|
||||
- d7_menu
|
||||
source: menu_name
|
||||
-
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
-
|
||||
plugin: static_map
|
||||
map:
|
||||
management: admin
|
||||
bypass: true
|
||||
# In this process pipeline, given a menu link path that might be for a
|
||||
# translated node which has been merged with the default language node, we are
|
||||
# trying to determine the new node ID, that is the ID of the default language
|
||||
# node.
|
||||
new_nid:
|
||||
-
|
||||
# If the path is of the form "node/<ID>" and is not routed, we will get
|
||||
# back a URI of the form "base:node/<ID>".
|
||||
plugin: link_uri
|
||||
source: link_path
|
||||
validate_route: false
|
||||
-
|
||||
# Isolate the node ID.
|
||||
plugin: explode
|
||||
delimiter: 'base:node/'
|
||||
-
|
||||
# Extract the node ID.
|
||||
plugin: extract
|
||||
default: false
|
||||
index:
|
||||
- 1
|
||||
-
|
||||
# Skip row if node ID is empty.
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
-
|
||||
# With the old node ID in hand, lookup in the d6_node_translation or
|
||||
# d7_node_translation mapping tables to find the new node ID.
|
||||
plugin: migration_lookup
|
||||
migration:
|
||||
- d6_node_complete
|
||||
- d7_node_complete
|
||||
- d6_node_translation
|
||||
- d7_node_translation
|
||||
no_stub: true
|
||||
-
|
||||
# Skip row if the new node ID is empty.
|
||||
plugin: skip_on_empty
|
||||
method: row
|
||||
-
|
||||
# Extract the node ID. The migration lookup will return an array with two
|
||||
# items, the new node ID and the translation langcode. We need the node ID
|
||||
# which is at index 0.
|
||||
plugin: extract
|
||||
index:
|
||||
- 0
|
||||
# This will be used in the "link/uri" and "route" processes below.
|
||||
link_path:
|
||||
plugin: concat
|
||||
source:
|
||||
- 'constants/node_prefix'
|
||||
- '@new_nid'
|
||||
link/uri:
|
||||
plugin: concat
|
||||
source:
|
||||
- 'constants/entity_prefix'
|
||||
- '@link_path'
|
||||
link/options: options
|
||||
route:
|
||||
plugin: route
|
||||
source:
|
||||
- '@link_path'
|
||||
- options
|
||||
route_name: '@route/route_name'
|
||||
route_parameters: '@route/route_parameters'
|
||||
url: '@route/url'
|
||||
options: '@route/options'
|
||||
external: external
|
||||
weight: weight
|
||||
expanded: expanded
|
||||
enabled: enabled
|
||||
parent:
|
||||
plugin: menu_link_parent
|
||||
source:
|
||||
- plid
|
||||
- '@menu_name'
|
||||
- parent_link_path
|
||||
changed: updated
|
||||
destination:
|
||||
plugin: entity:menu_link_content
|
||||
default_bundle: menu_link_content
|
||||
no_stub: true
|
||||
migration_dependencies:
|
||||
required:
|
||||
- language
|
||||
optional:
|
||||
- d6_menu
|
||||
- d6_menu_links
|
||||
- d6_node_translation
|
||||
- d7_menu
|
||||
- d7_menu_links
|
||||
- d7_node_translation
|
||||
@ -0,0 +1,35 @@
|
||||
finished:
|
||||
6:
|
||||
i18n: content_translation
|
||||
i18nblocks:
|
||||
- block
|
||||
- block_content
|
||||
- content_translation
|
||||
i18ncontent: content_translation
|
||||
i18nmenu:
|
||||
- content_translation
|
||||
- menu_link_content
|
||||
i18npoll: content_translation
|
||||
i18nstrings: content_translation
|
||||
i18nsync: content_translation
|
||||
i18ntaxonomy: content_translation
|
||||
locale: content_translation
|
||||
menu: content_translation
|
||||
# Node revision translations.
|
||||
node: content_translation
|
||||
taxonomy: content_translation
|
||||
7:
|
||||
entity_translation: content_translation
|
||||
i18n: content_translation
|
||||
i18n_block:
|
||||
- block
|
||||
- block_content
|
||||
- content_translation
|
||||
locale: content_translation
|
||||
menu: content_translation
|
||||
# menu links.
|
||||
i18n_menu: content_translation
|
||||
i18n_string: content_translation
|
||||
i18n_taxonomy: content_translation
|
||||
# Node revision translations.
|
||||
node: content_translation
|
||||
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Access;
|
||||
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\language\Entity\ContentLanguageSettings;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Access check for entity translation deletion.
|
||||
*
|
||||
* @internal This additional access checker only aims to prevent deletions in
|
||||
* pending revisions until we are able to flag revision translations as
|
||||
* deleted.
|
||||
*
|
||||
* @todo Remove this in https://www.drupal.org/node/2945956.
|
||||
*/
|
||||
class ContentTranslationDeleteAccess implements AccessInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* Constructs a ContentTranslationDeleteAccess object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
|
||||
* The content translation manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $manager, ContentTranslationManagerInterface $content_translation_manager) {
|
||||
$this->entityTypeManager = $manager;
|
||||
$this->contentTranslationManager = $content_translation_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access to translation deletion for the specified route match.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The parameterized route.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The currently logged in account.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
|
||||
$requirement = $route_match->getRouteObject()->getRequirement('_access_content_translation_delete');
|
||||
$entity_type_id = current(explode('.', $requirement));
|
||||
$entity = $route_match->getParameter($entity_type_id);
|
||||
return $this->checkAccess($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access to translation deletion for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity translation to be deleted.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function checkAccess(ContentEntityInterface $entity) {
|
||||
$result = AccessResult::allowed();
|
||||
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$result->addCacheableDependency($entity);
|
||||
// Add the cache dependencies used by
|
||||
// ContentTranslationManager::isPendingRevisionSupportEnabled().
|
||||
if (\Drupal::moduleHandler()->moduleExists('content_moderation')) {
|
||||
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
|
||||
$result->addCacheableDependency($workflow);
|
||||
}
|
||||
}
|
||||
if (!ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($entity->isDefaultTranslation()) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$config = ContentLanguageSettings::load($entity_type_id . '.' . $entity->bundle());
|
||||
$result->addCacheableDependency($config);
|
||||
if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
$revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $entity->language()->getId());
|
||||
if (!$revision_id) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||
$revision = $storage->loadRevision($revision_id);
|
||||
if ($revision->wasDefaultRevision()) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result = $result->andIf(AccessResult::forbidden());
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Access check for entity translation CRUD operation.
|
||||
*/
|
||||
class ContentTranslationManageAccessCheck implements AccessInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*
|
||||
* @var \Drupal\Core\Language\LanguageManagerInterface
|
||||
*/
|
||||
protected $languageManager;
|
||||
|
||||
/**
|
||||
* Constructs a ContentTranslationManageAccessCheck object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
* The language manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->languageManager = $language_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks translation access for the entity and operation on the given route.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\Route $route
|
||||
* The route to check against.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The parametrized route.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The currently logged in account.
|
||||
* @param string $source
|
||||
* (optional) For a create operation, the language code of the source.
|
||||
* @param string $target
|
||||
* (optional) For a create operation, the language code of the translation.
|
||||
* @param string $language
|
||||
* (optional) For an update or delete operation, the language code of the
|
||||
* translation being updated or deleted.
|
||||
* @param string $entity_type_id
|
||||
* (optional) The entity type ID.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account, $source = NULL, $target = NULL, $language = NULL, $entity_type_id = NULL) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
if ($entity = $route_match->getParameter($entity_type_id)) {
|
||||
$operation = $route->getRequirement('_access_content_translation_manage');
|
||||
$language = $this->languageManager->getLanguage($language) ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
|
||||
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
|
||||
|
||||
if (in_array($operation, ['update', 'delete'])) {
|
||||
// Translation operations cannot be performed on the default
|
||||
// translation.
|
||||
if ($language->getId() == $entity->getUntranslated()->language()->getId()) {
|
||||
return AccessResult::forbidden()->addCacheableDependency($entity);
|
||||
}
|
||||
// Editors have no access to the translation operations, as entity
|
||||
// access already grants them an equal or greater access level.
|
||||
$templates = ['update' => 'edit-form', 'delete' => 'delete-form'];
|
||||
if ($entity->access($operation) && $entity_type->hasLinkTemplate($templates[$operation])) {
|
||||
return AccessResult::forbidden()->cachePerPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
if ($account->hasPermission('translate any entity')) {
|
||||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
|
||||
switch ($operation) {
|
||||
case 'create':
|
||||
/** @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
|
||||
$handler = $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'translation');
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
$languages = $this->languageManager->getLanguages();
|
||||
$source_language = $this->languageManager->getLanguage($source) ?: $entity->language();
|
||||
$target_language = $this->languageManager->getLanguage($target) ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
|
||||
$is_new_translation = ($source_language->getId() != $target_language->getId()
|
||||
&& isset($languages[$source_language->getId()])
|
||||
&& isset($languages[$target_language->getId()])
|
||||
&& !isset($translations[$target_language->getId()]));
|
||||
return AccessResult::allowedIf($is_new_translation)->cachePerPermissions()->addCacheableDependency($entity)
|
||||
->andIf($handler->getTranslationAccess($entity, $operation));
|
||||
|
||||
case 'delete':
|
||||
// @todo Remove this in https://www.drupal.org/node/2945956.
|
||||
/** @var \Drupal\Core\Access\AccessResultInterface $delete_access */
|
||||
$delete_access = \Drupal::service('content_translation.delete_access')->checkAccess($entity);
|
||||
$access = $this->checkAccess($entity, $language, $operation);
|
||||
return $delete_access->andIf($access);
|
||||
|
||||
case 'update':
|
||||
return $this->checkAccess($entity, $language, $operation);
|
||||
}
|
||||
}
|
||||
|
||||
// No opinion.
|
||||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs access checks for the specified operation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being checked.
|
||||
* @param \Drupal\Core\Language\LanguageInterface $language
|
||||
* For an update or delete operation, the language code of the translation
|
||||
* being updated or deleted.
|
||||
* @param string $operation
|
||||
* The operation to be checked.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* An access result object.
|
||||
*/
|
||||
protected function checkAccess(ContentEntityInterface $entity, LanguageInterface $language, $operation) {
|
||||
/** @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
|
||||
$handler = $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'translation');
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
$languages = $this->languageManager->getLanguages();
|
||||
$has_translation = isset($languages[$language->getId()])
|
||||
&& $language->getId() != $entity->getUntranslated()->language()->getId()
|
||||
&& isset($translations[$language->getId()]);
|
||||
return AccessResult::allowedIf($has_translation)->cachePerPermissions()->addCacheableDependency($entity)
|
||||
->andIf($handler->getTranslationAccess($entity, $operation));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Access check for entity translation overview.
|
||||
*/
|
||||
class ContentTranslationOverviewAccess implements AccessInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a ContentTranslationOverviewAccess object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access to the translation overview for the entity and bundle.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The parametrized route.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The currently logged in account.
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function access(RouteMatchInterface $route_match, AccountInterface $account, $entity_type_id) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $route_match->getParameter($entity_type_id);
|
||||
if ($entity && $entity->isTranslatable()) {
|
||||
// Get entity base info.
|
||||
$bundle = $entity->bundle();
|
||||
|
||||
// Get entity access callback.
|
||||
$definition = $this->entityTypeManager->getDefinition($entity_type_id);
|
||||
$translation = $definition->get('translation');
|
||||
$access_callback = $translation['content_translation']['access_callback'];
|
||||
$access = call_user_func($access_callback, $entity);
|
||||
if ($access->isAllowed()) {
|
||||
return $access;
|
||||
}
|
||||
|
||||
// Check "translate any entity" permission.
|
||||
if ($account->hasPermission('translate any entity')) {
|
||||
return AccessResult::allowed()->cachePerPermissions()->inheritCacheability($access);
|
||||
}
|
||||
|
||||
// Check per entity permission.
|
||||
$permission = "translate {$entity_type_id}";
|
||||
if ($definition->getPermissionGranularity() == 'bundle') {
|
||||
$permission = "translate {$bundle} {$entity_type_id}";
|
||||
}
|
||||
return AccessResult::allowedIfHasPermission($account, $permission)->inheritCacheability($access);
|
||||
}
|
||||
|
||||
// No opinion.
|
||||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
/**
|
||||
* Interface providing support for content translation bundle settings.
|
||||
*/
|
||||
interface BundleTranslationSettingsInterface {
|
||||
|
||||
/**
|
||||
* Returns translation settings for the specified bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
* @param string $bundle
|
||||
* The bundle name.
|
||||
*
|
||||
* @return array
|
||||
* An associative array of values keyed by setting name.
|
||||
*/
|
||||
public function getBundleTranslationSettings($entity_type_id, $bundle);
|
||||
|
||||
/**
|
||||
* Sets translation settings for the specified bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
* @param string $bundle
|
||||
* The bundle name.
|
||||
* @param array $settings
|
||||
* An associative array of values keyed by setting name.
|
||||
*/
|
||||
public function setBundleTranslationSettings($entity_type_id, $bundle, array $settings);
|
||||
|
||||
}
|
||||
@ -0,0 +1,791 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Datetime\DateFormatterInterface;
|
||||
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
||||
use Drupal\Core\Entity\EntityChangedInterface;
|
||||
use Drupal\Core\Entity\EntityChangesDetectionTrait;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Routing\RedirectDestinationInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\user\EntityOwnerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Base class for content translation handlers.
|
||||
*
|
||||
* @ingroup entity_api
|
||||
*/
|
||||
class ContentTranslationHandler implements ContentTranslationHandlerInterface, EntityHandlerInterface {
|
||||
|
||||
use EntityChangesDetectionTrait;
|
||||
use DependencySerializationTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The type of the entity being translated.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId;
|
||||
|
||||
/**
|
||||
* Installed field storage definitions for the entity type.
|
||||
*
|
||||
* Keyed by field name.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
|
||||
*/
|
||||
protected $fieldStorageDefinitions;
|
||||
|
||||
/**
|
||||
* Initializes an instance of the content translation controller.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
|
||||
* The info array of the given entity type.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
|
||||
* The language manager.
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $manager
|
||||
* The content translation manager service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Session\AccountInterface $currentUser
|
||||
* The current user.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
* @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter
|
||||
* The date formatter service.
|
||||
* @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository
|
||||
* The installed entity definition repository service.
|
||||
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirectDestination
|
||||
* The request stack.
|
||||
* @param \Drupal\Component\Datetime\TimeInterface $time
|
||||
* The time service.
|
||||
*/
|
||||
public function __construct(
|
||||
protected EntityTypeInterface $entityType,
|
||||
protected LanguageManagerInterface $languageManager,
|
||||
protected ContentTranslationManagerInterface $manager,
|
||||
protected EntityTypeManagerInterface $entityTypeManager,
|
||||
protected AccountInterface $currentUser,
|
||||
protected MessengerInterface $messenger,
|
||||
protected DateFormatterInterface $dateFormatter,
|
||||
protected EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository,
|
||||
protected RedirectDestinationInterface $redirectDestination,
|
||||
protected TimeInterface $time,
|
||||
) {
|
||||
$this->entityTypeId = $entityType->id();
|
||||
$this->fieldStorageDefinitions = $entity_last_installed_schema_repository->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$entity_type,
|
||||
$container->get('language_manager'),
|
||||
$container->get('content_translation.manager'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('current_user'),
|
||||
$container->get('messenger'),
|
||||
$container->get('date.formatter'),
|
||||
$container->get('entity.last_installed_schema.repository'),
|
||||
$container->get('redirect.destination'),
|
||||
$container->get('datetime.time'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFieldDefinitions() {
|
||||
$definitions = [];
|
||||
|
||||
$definitions['content_translation_source'] = BaseFieldDefinition::create('language')
|
||||
->setLabel($this->t('Translation source'))
|
||||
->setDescription($this->t('The source language from which this translation was created.'))
|
||||
->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED)
|
||||
->setInitialValue(LanguageInterface::LANGCODE_NOT_SPECIFIED)
|
||||
->setRevisionable(TRUE)
|
||||
->setTranslatable(TRUE);
|
||||
|
||||
$definitions['content_translation_outdated'] = BaseFieldDefinition::create('boolean')
|
||||
->setLabel($this->t('Translation outdated'))
|
||||
->setDescription($this->t('A boolean indicating whether this translation needs to be updated.'))
|
||||
->setDefaultValue(FALSE)
|
||||
->setInitialValue(FALSE)
|
||||
->setRevisionable(TRUE)
|
||||
->setTranslatable(TRUE);
|
||||
|
||||
if (!$this->hasAuthor()) {
|
||||
$definitions['content_translation_uid'] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel($this->t('Translation author'))
|
||||
->setDescription($this->t('The author of this translation.'))
|
||||
->setSetting('target_type', 'user')
|
||||
->setSetting('handler', 'default')
|
||||
->setRevisionable(TRUE)
|
||||
->setDefaultValueCallback(static::class . '::getDefaultOwnerId')
|
||||
->setTranslatable(TRUE);
|
||||
}
|
||||
|
||||
if (!$this->hasPublishedStatus()) {
|
||||
$definitions['content_translation_status'] = BaseFieldDefinition::create('boolean')
|
||||
->setLabel($this->t('Translation status'))
|
||||
->setDescription($this->t('A boolean indicating whether the translation is visible to non-translators.'))
|
||||
->setDefaultValue(TRUE)
|
||||
->setInitialValue(TRUE)
|
||||
->setRevisionable(TRUE)
|
||||
->setTranslatable(TRUE);
|
||||
}
|
||||
|
||||
if (!$this->hasCreatedTime()) {
|
||||
$definitions['content_translation_created'] = BaseFieldDefinition::create('created')
|
||||
->setLabel($this->t('Translation created time'))
|
||||
->setDescription($this->t('The Unix timestamp when the translation was created.'))
|
||||
->setRevisionable(TRUE)
|
||||
->setTranslatable(TRUE);
|
||||
}
|
||||
|
||||
if (!$this->hasChangedTime()) {
|
||||
$definitions['content_translation_changed'] = BaseFieldDefinition::create('changed')
|
||||
->setLabel($this->t('Translation changed time'))
|
||||
->setDescription($this->t('The Unix timestamp when the translation was most recently saved.'))
|
||||
->setRevisionable(TRUE)
|
||||
->setTranslatable(TRUE);
|
||||
}
|
||||
|
||||
return $definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the entity type supports author natively.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if metadata is natively supported, FALSE otherwise.
|
||||
*/
|
||||
protected function hasAuthor() {
|
||||
// Check for field named uid, but only in case the entity implements the
|
||||
// EntityOwnerInterface. This helps to exclude cases, where the uid is
|
||||
// defined as field name, but is not meant to be an owner field; for
|
||||
// instance, the User entity.
|
||||
return $this->entityType->entityClassImplements(EntityOwnerInterface::class) && $this->checkFieldStorageDefinitionTranslatability('uid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the entity type supports published status natively.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if metadata is natively supported, FALSE otherwise.
|
||||
*/
|
||||
protected function hasPublishedStatus() {
|
||||
return $this->checkFieldStorageDefinitionTranslatability('status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the entity type supports modification time natively.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if metadata is natively supported, FALSE otherwise.
|
||||
*/
|
||||
protected function hasChangedTime() {
|
||||
return $this->entityType->entityClassImplements(EntityChangedInterface::class) && $this->checkFieldStorageDefinitionTranslatability('changed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the entity type supports creation time natively.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if metadata is natively supported, FALSE otherwise.
|
||||
*/
|
||||
protected function hasCreatedTime() {
|
||||
return $this->checkFieldStorageDefinitionTranslatability('created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the field storage definition for translatability support.
|
||||
*
|
||||
* Checks whether the given field is defined in the field storage definitions
|
||||
* and if its definition specifies it as translatable.
|
||||
*
|
||||
* @param string $field_name
|
||||
* The name of the field.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if translatable field storage definition exists, FALSE otherwise.
|
||||
*/
|
||||
protected function checkFieldStorageDefinitionTranslatability($field_name) {
|
||||
return array_key_exists($field_name, $this->fieldStorageDefinitions) && $this->fieldStorageDefinitions[$field_name]->isTranslatable();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function retranslate(EntityInterface $entity, $langcode = NULL) {
|
||||
$updated_langcode = !empty($langcode) ? $langcode : $entity->language()->getId();
|
||||
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
|
||||
$this->manager->getTranslationMetadata($entity->getTranslation($langcode))
|
||||
->setOutdated($langcode != $updated_langcode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTranslationAccess(EntityInterface $entity, $op) {
|
||||
// @todo Move this logic into a translation access control handler checking also
|
||||
// the translation language and the given account.
|
||||
$entity_type = $entity->getEntityType();
|
||||
$translate_permission = TRUE;
|
||||
// If no permission granularity is defined this entity type does not need an
|
||||
// explicit translate permission.
|
||||
if (!$this->currentUser->hasPermission('translate any entity') && $permission_granularity = $entity_type->getPermissionGranularity()) {
|
||||
$translate_permission = $this->currentUser->hasPermission($permission_granularity == 'bundle' ? "translate {$entity->bundle()} {$entity->getEntityTypeId()}" : "translate {$entity->getEntityTypeId()}");
|
||||
}
|
||||
$access = AccessResult::allowedIf(($translate_permission && $this->currentUser->hasPermission("$op content translations")))->cachePerPermissions();
|
||||
if (!$access->isAllowed()) {
|
||||
return AccessResult::allowedIfHasPermission($this->currentUser, 'translate editable entities')->andIf($entity->access('update', $this->currentUser, TRUE));
|
||||
}
|
||||
return $access;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSourceLangcode(FormStateInterface $form_state) {
|
||||
if ($source = $form_state->get(['content_translation', 'source'])) {
|
||||
return $source->getId();
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
|
||||
$metadata = $this->manager->getTranslationMetadata($entity);
|
||||
$form_object = $form_state->getFormObject();
|
||||
$form_langcode = $form_object->getFormLangcode($form_state);
|
||||
$entity_langcode = $entity->getUntranslated()->language()->getId();
|
||||
|
||||
$new_translation = $entity->isNewTranslation();
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
if ($new_translation) {
|
||||
// Make sure a new translation does not appear as existing yet.
|
||||
unset($translations[$form_langcode]);
|
||||
}
|
||||
$is_translation = $new_translation || ($entity->language()->getId() != $entity_langcode);
|
||||
$has_translations = count($translations) > 1;
|
||||
|
||||
// Adjust page title to specify the current language being edited, if we
|
||||
// have at least one translation.
|
||||
$languages = $this->languageManager->getLanguages();
|
||||
if (isset($languages[$form_langcode]) && ($has_translations || $new_translation)) {
|
||||
$title = $this->entityFormTitle($entity);
|
||||
// When editing the original values display just the entity label.
|
||||
if ($is_translation) {
|
||||
$t_args = ['%language' => $languages[$form_langcode]->getName(), '%title' => $entity->label(), '@title' => $title];
|
||||
$title = $new_translation ? $this->t('Create %language translation of %title', $t_args) : $this->t('@title [%language translation]', $t_args);
|
||||
}
|
||||
$form['#title'] = $title;
|
||||
}
|
||||
|
||||
// Display source language selector only if we are creating a new
|
||||
// translation and there are at least two translations available.
|
||||
if ($has_translations && $new_translation) {
|
||||
$source_langcode = $metadata->getSource();
|
||||
$form['source_langcode'] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Source language: @language', ['@language' => $languages[$source_langcode]->getName()]),
|
||||
'#tree' => TRUE,
|
||||
'#weight' => -100,
|
||||
'#multilingual' => TRUE,
|
||||
'source' => [
|
||||
'#title' => $this->t('Select source language'),
|
||||
'#title_display' => 'invisible',
|
||||
'#type' => 'select',
|
||||
'#default_value' => $source_langcode,
|
||||
'#options' => [],
|
||||
],
|
||||
'submit' => [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Change'),
|
||||
'#submit' => [[$this, 'entityFormSourceChange']],
|
||||
],
|
||||
];
|
||||
foreach ($this->languageManager->getLanguages() as $language) {
|
||||
if (isset($translations[$language->getId()])) {
|
||||
$form['source_langcode']['source']['#options'][$language->getId()] = $language->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Locate the language widget.
|
||||
$langcode_key = $this->entityType->getKey('langcode');
|
||||
if (isset($form[$langcode_key])) {
|
||||
$language_widget = &$form[$langcode_key];
|
||||
}
|
||||
|
||||
// If we are editing the source entity, limit the list of languages so that
|
||||
// it is not possible to switch to a language for which a translation
|
||||
// already exists. Note that this will only work if the widget is structured
|
||||
// like \Drupal\Core\Field\Plugin\Field\FieldWidget\LanguageSelectWidget.
|
||||
if (isset($language_widget['widget'][0]['value']) && !$is_translation && $has_translations) {
|
||||
$language_select = &$language_widget['widget'][0]['value'];
|
||||
if ($language_select['#type'] == 'language_select') {
|
||||
$options = [];
|
||||
foreach ($this->languageManager->getLanguages() as $language) {
|
||||
// Show the current language, and the languages for which no
|
||||
// translation already exists.
|
||||
if (empty($translations[$language->getId()]) || $language->getId() == $entity_langcode) {
|
||||
$options[$language->getId()] = $language->getName();
|
||||
}
|
||||
}
|
||||
$language_select['#options'] = $options;
|
||||
}
|
||||
}
|
||||
if ($is_translation) {
|
||||
if (isset($language_widget)) {
|
||||
$language_widget['widget']['#access'] = FALSE;
|
||||
}
|
||||
|
||||
// Replace the delete button with the delete translation one.
|
||||
if (!$new_translation) {
|
||||
$weight = 100;
|
||||
foreach (['delete', 'submit'] as $key) {
|
||||
if (isset($form['actions'][$key]['weight'])) {
|
||||
$weight = $form['actions'][$key]['weight'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
/** @var \Drupal\Core\Access\AccessResultInterface $delete_access */
|
||||
$delete_access = \Drupal::service('content_translation.delete_access')->checkAccess($entity);
|
||||
$access = $delete_access->isAllowed() && (
|
||||
$this->getTranslationAccess($entity, 'delete')->isAllowed() ||
|
||||
($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form'))
|
||||
);
|
||||
$form['actions']['delete_translation'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Delete translation'),
|
||||
'#access' => $access,
|
||||
'#weight' => $weight,
|
||||
'#url' => $this->entityFormDeleteTranslationUrl($entity, $form_langcode),
|
||||
'#attributes' => [
|
||||
'class' => ['button', 'button--danger'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Always remove the delete button on translation forms.
|
||||
unset($form['actions']['delete']);
|
||||
}
|
||||
|
||||
// We need to display the translation tab only when there is at least one
|
||||
// translation available or a new one is about to be created.
|
||||
if ($new_translation || $has_translations) {
|
||||
$form['content_translation'] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Translation'),
|
||||
'#tree' => TRUE,
|
||||
'#weight' => 10,
|
||||
'#access' => $this->getTranslationAccess($entity, $new_translation ? 'create' : 'update')->isAllowed(),
|
||||
'#multilingual' => TRUE,
|
||||
];
|
||||
|
||||
if (isset($form['advanced'])) {
|
||||
$form['content_translation'] += [
|
||||
'#group' => 'advanced',
|
||||
'#weight' => 100,
|
||||
'#attributes' => [
|
||||
'class' => ['entity-translation-options'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// A new translation is enabled by default.
|
||||
$status = $new_translation || $metadata->isPublished();
|
||||
// If there is only one published translation we cannot unpublish it,
|
||||
// since there would be nothing left to display.
|
||||
$enabled = TRUE;
|
||||
if ($status) {
|
||||
$published = 0;
|
||||
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
|
||||
$published += $this->manager->getTranslationMetadata($entity->getTranslation($langcode))
|
||||
->isPublished();
|
||||
}
|
||||
$enabled = $published > 1;
|
||||
}
|
||||
$description = $enabled ?
|
||||
$this->t('An unpublished translation will not be visible without translation permissions.') :
|
||||
$this->t('Only this translation is published. You must publish at least one more translation to unpublish this one.');
|
||||
|
||||
$form['content_translation']['status'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('This translation is published'),
|
||||
'#default_value' => $status,
|
||||
'#description' => $description,
|
||||
'#disabled' => !$enabled,
|
||||
];
|
||||
|
||||
$translate = !$new_translation && $metadata->isOutdated();
|
||||
$outdated_access = !ContentTranslationManager::isPendingRevisionSupportEnabled($entity->getEntityTypeId(), $entity->bundle());
|
||||
if (!$outdated_access) {
|
||||
$form['content_translation']['outdated'] = [
|
||||
'#markup' => $this->t('Translations cannot be flagged as outdated when content is moderated.'),
|
||||
];
|
||||
}
|
||||
elseif (!$translate) {
|
||||
$form['content_translation']['retranslate'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Flag other translations as outdated'),
|
||||
'#default_value' => FALSE,
|
||||
'#description' => $this->t('If you made a significant change, which means the other translations should be updated, you can flag all translations of this content as outdated. This will not change any other property of them, like whether they are published or not.'),
|
||||
'#access' => $outdated_access,
|
||||
];
|
||||
}
|
||||
else {
|
||||
$form['content_translation']['outdated'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('This translation needs to be updated'),
|
||||
'#default_value' => $translate,
|
||||
'#description' => $this->t('When this option is checked, this translation needs to be updated. Uncheck when the translation is up to date again.'),
|
||||
'#access' => $outdated_access,
|
||||
];
|
||||
$form['content_translation']['#open'] = TRUE;
|
||||
}
|
||||
|
||||
// Default to the anonymous user.
|
||||
$uid = 0;
|
||||
if ($new_translation) {
|
||||
$uid = $this->currentUser->id();
|
||||
}
|
||||
elseif (($account = $metadata->getAuthor()) && $account->id()) {
|
||||
$uid = $account->id();
|
||||
}
|
||||
$form['content_translation']['uid'] = [
|
||||
'#type' => 'entity_autocomplete',
|
||||
'#title' => $this->t('Authored by'),
|
||||
'#target_type' => 'user',
|
||||
'#default_value' => User::load($uid),
|
||||
// Validation is done by static::entityFormValidate().
|
||||
'#validate_reference' => FALSE,
|
||||
'#maxlength' => 1024,
|
||||
'#description' => $this->t('Leave blank for %anonymous.', ['%anonymous' => \Drupal::config('user.settings')->get('anonymous')]),
|
||||
];
|
||||
|
||||
$date = $new_translation ? $this->time->getRequestTime() : $metadata->getCreatedTime();
|
||||
$form['content_translation']['created'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Authored on'),
|
||||
'#maxlength' => 25,
|
||||
'#description' => $this->t('Leave blank to use the time of form submission.'),
|
||||
'#default_value' => $new_translation || !$date ? '' : $this->dateFormatter->format($date, 'custom', 'Y-m-d H:i:s O'),
|
||||
];
|
||||
|
||||
$form['#process'][] = [$this, 'entityFormSharedElements'];
|
||||
}
|
||||
|
||||
// Process the submitted values before they are stored.
|
||||
$form['#entity_builders'][] = [$this, 'entityFormEntityBuild'];
|
||||
|
||||
// Handle entity validation.
|
||||
$form['#validate'][] = [$this, 'entityFormValidate'];
|
||||
|
||||
// Handle entity deletion.
|
||||
if (isset($form['actions']['delete'])) {
|
||||
$form['actions']['delete']['#submit'][] = [$this, 'entityFormDelete'];
|
||||
}
|
||||
|
||||
// Handle entity form submission before the entity has been saved.
|
||||
foreach (Element::children($form['actions']) as $action) {
|
||||
if (isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] == 'submit') {
|
||||
array_unshift($form['actions'][$action]['#submit'], [$this, 'entityFormSubmit']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process callback: determines which elements get clue in the form.
|
||||
*
|
||||
* @see \Drupal\content_translation\ContentTranslationHandler::entityFormAlter()
|
||||
*/
|
||||
public function entityFormSharedElements($element, FormStateInterface $form_state, $form) {
|
||||
static $ignored_types;
|
||||
|
||||
// @todo Find a more reliable way to determine if a form element concerns a
|
||||
// multilingual value.
|
||||
if (!isset($ignored_types)) {
|
||||
$ignored_types = array_flip(['actions', 'value', 'hidden', 'vertical_tabs', 'token', 'details', 'link']);
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityForm $form_object */
|
||||
$form_object = $form_state->getFormObject();
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $form_object->getEntity();
|
||||
$display_translatability_clue = !$entity->isDefaultTranslationAffectedOnly();
|
||||
$hide_untranslatable_fields = $entity->isDefaultTranslationAffectedOnly() && !$entity->isDefaultTranslation();
|
||||
$translation_form = $form_state->get(['content_translation', 'translation_form']);
|
||||
$display_warning = FALSE;
|
||||
|
||||
// We use field definitions to identify untranslatable field widgets to be
|
||||
// hidden. Fields that are not involved in translation changes checks should
|
||||
// not be affected by this logic (the "revision_log" field, for instance).
|
||||
$field_definitions = array_diff_key($entity->getFieldDefinitions(), array_flip($this->getFieldsToSkipFromTranslationChangesCheck($entity)));
|
||||
|
||||
foreach (Element::children($element) as $key) {
|
||||
if (!isset($element[$key]['#type'])) {
|
||||
$this->entityFormSharedElements($element[$key], $form_state, $form);
|
||||
}
|
||||
else {
|
||||
// Ignore non-widget form elements.
|
||||
if (isset($ignored_types[$element[$key]['#type']])) {
|
||||
continue;
|
||||
}
|
||||
// Elements are considered to be non multilingual by default.
|
||||
if (empty($element[$key]['#multilingual'])) {
|
||||
// If we are displaying a multilingual entity form we need to provide
|
||||
// translatability clues, otherwise the non-multilingual form elements
|
||||
// should be hidden.
|
||||
if (!$translation_form) {
|
||||
if ($display_translatability_clue) {
|
||||
$this->addTranslatabilityClue($element[$key]);
|
||||
}
|
||||
// Hide widgets for untranslatable fields.
|
||||
if ($hide_untranslatable_fields && isset($field_definitions[$key])) {
|
||||
$element[$key]['#access'] = FALSE;
|
||||
$display_warning = TRUE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$element[$key]['#access'] = FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($display_warning) {
|
||||
$url = $entity->getUntranslated()->toUrl('edit-form')->toString();
|
||||
$message['warning'][] = $this->t('Fields that apply to all languages are hidden to avoid conflicting changes. <a href=":url">Edit them on the original language form</a>.', [':url' => $url]);
|
||||
// Explicitly renders this warning message. This prevents repetition on
|
||||
// AJAX operations or form submission. Other messages will be rendered in
|
||||
// the default location.
|
||||
// @see \Drupal\Core\Render\Element\StatusMessages.
|
||||
$element['hidden_fields_warning_message'] = [
|
||||
'#theme' => 'status_messages',
|
||||
'#message_list' => $message,
|
||||
'#weight' => -100,
|
||||
'#status_headings' => [
|
||||
'warning' => $this->t('Warning message'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a clue about the form element translatability.
|
||||
*
|
||||
* If the given element does not have a #title attribute, the function is
|
||||
* recursively applied to child elements.
|
||||
*
|
||||
* @param array $element
|
||||
* A form element array.
|
||||
*/
|
||||
protected function addTranslatabilityClue(&$element) {
|
||||
static $suffix, $fapi_title_elements;
|
||||
|
||||
// Elements which can have a #title attribute according to FAPI Reference.
|
||||
if (!isset($suffix)) {
|
||||
$suffix = ' <span class="translation-entity-all-languages">(' . $this->t('all languages') . ')</span>';
|
||||
$fapi_title_elements = array_flip(['checkbox', 'checkboxes', 'date', 'details', 'fieldset', 'file', 'item', 'password', 'password_confirm', 'radio', 'radios', 'select', 'text_format', 'textarea', 'textfield', 'weight']);
|
||||
}
|
||||
|
||||
// Update #title attribute for all elements that are allowed to have a
|
||||
// #title attribute according to the Form API Reference. The reason for this
|
||||
// check is because some elements have a #title attribute even though it is
|
||||
// not rendered; for instance, field containers.
|
||||
if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) {
|
||||
$element['#title'] .= $suffix;
|
||||
}
|
||||
// If the current element does not have a (valid) title, try child elements.
|
||||
elseif ($children = Element::children($element)) {
|
||||
foreach ($children as $delta) {
|
||||
$this->addTranslatabilityClue($element[$delta]);
|
||||
}
|
||||
}
|
||||
// If there are no children, fall back to the current #title attribute if it
|
||||
// exists.
|
||||
elseif (isset($element['#title'])) {
|
||||
$element['#title'] .= $suffix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity builder method.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The type of the entity.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity whose form is being built.
|
||||
* @param array $form
|
||||
* A nested array form elements comprising the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*
|
||||
* @see \Drupal\content_translation\ContentTranslationHandler::entityFormAlter()
|
||||
*/
|
||||
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) {
|
||||
$form_object = $form_state->getFormObject();
|
||||
$form_langcode = $form_object->getFormLangcode($form_state);
|
||||
$values = &$form_state->getValue('content_translation', []);
|
||||
|
||||
$metadata = $this->manager->getTranslationMetadata($entity);
|
||||
$metadata->setAuthor(!empty($values['uid']) ? User::load($values['uid']) : User::load(0));
|
||||
$metadata->setPublished(!empty($values['status']));
|
||||
$metadata->setCreatedTime(!empty($values['created']) ? strtotime($values['created']) : $this->time->getRequestTime());
|
||||
|
||||
$metadata->setOutdated(!empty($values['outdated']));
|
||||
if (!empty($values['retranslate'])) {
|
||||
$this->retranslate($entity, $form_langcode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for ContentTranslationHandler::entityFormAlter().
|
||||
*
|
||||
* Validates the submitted content translation metadata.
|
||||
*/
|
||||
public function entityFormValidate($form, FormStateInterface $form_state) {
|
||||
if (!$form_state->isValueEmpty('content_translation')) {
|
||||
$translation = $form_state->getValue('content_translation');
|
||||
// Validate the "authored by" field.
|
||||
if (!empty($translation['uid']) && !($account = User::load($translation['uid']))) {
|
||||
$form_state->setErrorByName('content_translation][uid', $this->t('The translation authoring username %name does not exist.', ['%name' => $account->getAccountName()]));
|
||||
}
|
||||
// Validate the "authored on" field.
|
||||
if (!empty($translation['created']) && strtotime($translation['created']) === FALSE) {
|
||||
$form_state->setErrorByName('content_translation][created', $this->t('You have to specify a valid translation authoring date.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for ContentTranslationHandler::entityFormAlter().
|
||||
*
|
||||
* Updates metadata fields, which should be updated only after the validation
|
||||
* has run and before the entity is saved.
|
||||
*/
|
||||
public function entityFormSubmit($form, FormStateInterface $form_state) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
|
||||
$form_object = $form_state->getFormObject();
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $form_object->getEntity();
|
||||
|
||||
// ContentEntityForm::submit will update the changed timestamp on submit
|
||||
// after the entity has been validated, so that it does not break the
|
||||
// EntityChanged constraint validator. The content translation metadata
|
||||
// field for the changed timestamp does not have such a constraint defined
|
||||
// at the moment, but it is correct to update its value in a submission
|
||||
// handler as well and have the same logic like in the Form API.
|
||||
if ($entity->hasField('content_translation_changed')) {
|
||||
$metadata = $this->manager->getTranslationMetadata($entity);
|
||||
$metadata->setChangedTime($this->time->getRequestTime());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for ContentTranslationHandler::entityFormAlter().
|
||||
*
|
||||
* Takes care of the source language change.
|
||||
*/
|
||||
public function entityFormSourceChange($form, FormStateInterface $form_state) {
|
||||
$form_object = $form_state->getFormObject();
|
||||
$entity = $form_object->getEntity();
|
||||
$source = $form_state->getValue(['source_langcode', 'source']);
|
||||
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$form_state->setRedirect("entity.$entity_type_id.content_translation_add", [
|
||||
$entity_type_id => $entity->id(),
|
||||
'source' => $source,
|
||||
'target' => $form_object->getFormLangcode($form_state),
|
||||
]);
|
||||
$languages = $this->languageManager->getLanguages();
|
||||
$this->messenger->addStatus($this->t('Source language set to: %language', ['%language' => $languages[$source]->getName()]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for ContentTranslationHandler::entityFormAlter().
|
||||
*
|
||||
* Takes care of entity deletion.
|
||||
*/
|
||||
public function entityFormDelete($form, FormStateInterface $form_state) {
|
||||
$form_object = $form_state->getFormObject();
|
||||
$entity = $form_object->getEntity();
|
||||
if (count($entity->getTranslationLanguages()) > 1) {
|
||||
$this->messenger->addWarning($this->t('This will delete all the translations of %label.', ['%label' => $entity->label() ?? $entity->id()]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for ContentTranslationHandler::entityFormAlter().
|
||||
*
|
||||
* Get the entity delete form route url.
|
||||
*/
|
||||
protected function entityFormDeleteTranslationUrl(EntityInterface $entity, $form_langcode) {
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$options = [];
|
||||
$options['query']['destination'] = $this->redirectDestination->get();
|
||||
|
||||
if ($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form')) {
|
||||
return $entity->toUrl('delete-form', $options);
|
||||
}
|
||||
|
||||
return Url::fromRoute("entity.$entity_type_id.content_translation_delete", [
|
||||
$entity_type_id => $entity->id(),
|
||||
'language' => $form_langcode,
|
||||
], $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title to be used for the entity form page.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity whose form is being altered.
|
||||
*
|
||||
* @return string|null
|
||||
* The label of the entity, or NULL if there is no label defined.
|
||||
*/
|
||||
protected function entityFormTitle(EntityInterface $entity) {
|
||||
return $entity->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default value callback for the owner base field definition.
|
||||
*
|
||||
* @return int
|
||||
* The user ID.
|
||||
*/
|
||||
public static function getDefaultOwnerId() {
|
||||
return \Drupal::currentUser()->id();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Interface for providing content translation.
|
||||
*
|
||||
* Defines a set of methods to allow any entity to be processed by the entity
|
||||
* translation UI.
|
||||
*/
|
||||
interface ContentTranslationHandlerInterface {
|
||||
|
||||
/**
|
||||
* Returns a set of field definitions to be used to store metadata items.
|
||||
*
|
||||
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
|
||||
* The field definitions.
|
||||
*/
|
||||
public function getFieldDefinitions();
|
||||
|
||||
/**
|
||||
* Checks that the user can perform the operation on the entity translation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity whose translation has to be accessed.
|
||||
* @param string $op
|
||||
* The operation to be performed on the translation. Possible values are:
|
||||
* - "create".
|
||||
* - "update".
|
||||
* - "delete".
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function getTranslationAccess(EntityInterface $entity, $op);
|
||||
|
||||
/**
|
||||
* Retrieves the source language for the translation being created.
|
||||
*
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*
|
||||
* @return string
|
||||
* The source language code.
|
||||
*/
|
||||
public function getSourceLangcode(FormStateInterface $form_state);
|
||||
|
||||
/**
|
||||
* Marks translations as outdated.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being translated.
|
||||
* @param string $langcode
|
||||
* (optional) The language code of the updated language: all the other
|
||||
* translations will be marked as outdated. Defaults to the entity language.
|
||||
*/
|
||||
public function retranslate(EntityInterface $entity, $langcode = NULL);
|
||||
|
||||
/**
|
||||
* Performs the needed alterations to the entity form.
|
||||
*
|
||||
* @param array $form
|
||||
* The entity form to be altered to provide the translation workflow.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being created or edited.
|
||||
*/
|
||||
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity);
|
||||
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* Provides common functionality for content translation.
|
||||
*/
|
||||
class ContentTranslationManager implements ContentTranslationManagerInterface, BundleTranslationSettingsInterface {
|
||||
|
||||
/**
|
||||
* The entity type bundle info provider.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a ContentTranslationManageAccessCheck object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
|
||||
* The entity type bundle info provider.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityTypeBundleInfo = $entity_type_bundle_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTranslationHandler($entity_type_id) {
|
||||
return $this->entityTypeManager->getHandler($entity_type_id, 'translation');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTranslationMetadata(EntityInterface $translation) {
|
||||
// We need a new instance of the metadata handler wrapping each translation.
|
||||
$entity_type = $translation->getEntityType();
|
||||
$class = $entity_type->get('content_translation_metadata');
|
||||
return new $class($translation, $this->getTranslationHandler($entity_type->id()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSupported($entity_type_id) {
|
||||
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
|
||||
return $entity_type->isTranslatable() && ($entity_type->hasLinkTemplate('drupal:content-translation-overview') || $entity_type->get('content_translation_ui_skip'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSupportedEntityTypes() {
|
||||
$supported_types = [];
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
if ($this->isSupported($entity_type_id)) {
|
||||
$supported_types[$entity_type_id] = $entity_type;
|
||||
}
|
||||
}
|
||||
return $supported_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setEnabled($entity_type_id, $bundle, $value) {
|
||||
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
|
||||
$config->setThirdPartySetting('content_translation', 'enabled', $value)->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEnabled($entity_type_id, $bundle = NULL) {
|
||||
$enabled = FALSE;
|
||||
|
||||
if ($this->isSupported($entity_type_id)) {
|
||||
$bundles = !empty($bundle) ? [$bundle] : array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id));
|
||||
foreach ($bundles as $bundle) {
|
||||
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
|
||||
if ($config->getThirdPartySetting('content_translation', 'enabled', FALSE)) {
|
||||
$enabled = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setBundleTranslationSettings($entity_type_id, $bundle, array $settings) {
|
||||
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
|
||||
$config->setThirdPartySetting('content_translation', 'bundle_settings', $settings)
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBundleTranslationSettings($entity_type_id, $bundle) {
|
||||
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
|
||||
return $config->getThirdPartySetting('content_translation', 'bundle_settings', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a content language config entity based on the entity type and bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* ID of the entity type.
|
||||
* @param string $bundle
|
||||
* Bundle name.
|
||||
*
|
||||
* @return \Drupal\language\Entity\ContentLanguageSettings
|
||||
* The content language config entity if one exists. Otherwise, returns
|
||||
* default values.
|
||||
*/
|
||||
protected function loadContentLanguageSettings($entity_type_id, $bundle) {
|
||||
if ($entity_type_id == NULL || $bundle == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
$config = $this->entityTypeManager->getStorage('language_content_settings')->load($entity_type_id . '.' . $bundle);
|
||||
if ($config == NULL) {
|
||||
$config = $this->entityTypeManager->getStorage('language_content_settings')->create(['target_entity_type_id' => $entity_type_id, 'target_bundle' => $bundle]);
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether support for pending revisions should be enabled.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The ID of the entity type to be checked.
|
||||
* @param string $bundle_id
|
||||
* (optional) The ID of the bundle to be checked. Defaults to none.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if pending revisions should be enabled, FALSE otherwise.
|
||||
*
|
||||
* @internal
|
||||
* There is ongoing discussion about how pending revisions should behave.
|
||||
* The logic enabling pending revision support is likely to change once a
|
||||
* decision is made.
|
||||
*
|
||||
* @see https://www.drupal.org/node/2940575
|
||||
*/
|
||||
public static function isPendingRevisionSupportEnabled($entity_type_id, $bundle_id = NULL) {
|
||||
if (!\Drupal::moduleHandler()->moduleExists('content_moderation')) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
|
||||
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
|
||||
$plugin = $workflow->getTypePlugin();
|
||||
$entity_type_ids = array_flip($plugin->getEntityTypes());
|
||||
if (isset($entity_type_ids[$entity_type_id])) {
|
||||
if (!isset($bundle_id)) {
|
||||
return TRUE;
|
||||
}
|
||||
else {
|
||||
$bundle_ids = array_flip($plugin->getBundlesForEntityType($entity_type_id));
|
||||
if (isset($bundle_ids[$bundle_id])) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for common functionality for content translation.
|
||||
*/
|
||||
interface ContentTranslationManagerInterface {
|
||||
|
||||
/**
|
||||
* Gets the entity types that support content translation.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface[]
|
||||
* An array of entity types that support content translation.
|
||||
*/
|
||||
public function getSupportedEntityTypes();
|
||||
|
||||
/**
|
||||
* Checks whether an entity type supports translation.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if an entity type is supported, FALSE otherwise.
|
||||
*/
|
||||
public function isSupported($entity_type_id);
|
||||
|
||||
/**
|
||||
* Returns an instance of the Content translation handler.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The type of the entity being translated.
|
||||
*
|
||||
* @return \Drupal\content_translation\ContentTranslationHandlerInterface
|
||||
* An instance of the content translation handler.
|
||||
*/
|
||||
public function getTranslationHandler($entity_type_id);
|
||||
|
||||
/**
|
||||
* Returns an instance of the Content translation metadata.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $translation
|
||||
* The entity translation whose metadata needs to be retrieved.
|
||||
*
|
||||
* @return \Drupal\content_translation\ContentTranslationMetadataWrapperInterface
|
||||
* An instance of the content translation metadata.
|
||||
*/
|
||||
public function getTranslationMetadata(EntityInterface $translation);
|
||||
|
||||
/**
|
||||
* Sets the value for translatability of the given entity type bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type.
|
||||
* @param string $bundle
|
||||
* The bundle of the entity.
|
||||
* @param bool $value
|
||||
* The boolean value we need to save.
|
||||
*/
|
||||
public function setEnabled($entity_type_id, $bundle, $value);
|
||||
|
||||
/**
|
||||
* Determines whether the given entity type is translatable.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The type of the entity.
|
||||
* @param string $bundle
|
||||
* (optional) The bundle of the entity. If no bundle is provided, all the
|
||||
* available bundles are checked.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the specified bundle is translatable. If no bundle is provided
|
||||
* returns TRUE if at least one of the entity bundles is translatable.
|
||||
*/
|
||||
public function isEnabled($entity_type_id, $bundle = NULL);
|
||||
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\user\UserInterface;
|
||||
|
||||
/**
|
||||
* Base class for content translation metadata wrappers.
|
||||
*/
|
||||
class ContentTranslationMetadataWrapper implements ContentTranslationMetadataWrapperInterface {
|
||||
|
||||
/**
|
||||
* The wrapped entity translation.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\FieldableEntityInterface|\Drupal\Core\TypedData\TranslatableInterface
|
||||
*/
|
||||
protected $translation;
|
||||
|
||||
/**
|
||||
* The content translation handler.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationHandlerInterface
|
||||
*/
|
||||
protected $handler;
|
||||
|
||||
/**
|
||||
* Initializes an instance of the content translation metadata handler.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $translation
|
||||
* The entity translation to be wrapped.
|
||||
* @param ContentTranslationHandlerInterface $handler
|
||||
* The content translation handler.
|
||||
*/
|
||||
public function __construct(EntityInterface $translation, ContentTranslationHandlerInterface $handler) {
|
||||
$this->translation = $translation;
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSource() {
|
||||
return $this->translation->get('content_translation_source')->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSource($source) {
|
||||
$this->translation->set('content_translation_source', $source);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isOutdated() {
|
||||
return (bool) $this->translation->get('content_translation_outdated')->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOutdated($outdated) {
|
||||
$this->translation->set('content_translation_outdated', $outdated);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAuthor() {
|
||||
return $this->translation->hasField('content_translation_uid') ? $this->translation->get('content_translation_uid')->entity : $this->translation->getOwner();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setAuthor(UserInterface $account) {
|
||||
$field_name = $this->translation->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid';
|
||||
$this->setFieldOnlyIfTranslatable($field_name, $account->id());
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isPublished() {
|
||||
$field_name = $this->translation->hasField('content_translation_status') ? 'content_translation_status' : 'status';
|
||||
return (bool) $this->translation->get($field_name)->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setPublished($published) {
|
||||
$field_name = $this->translation->hasField('content_translation_status') ? 'content_translation_status' : 'status';
|
||||
$this->setFieldOnlyIfTranslatable($field_name, $published);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCreatedTime() {
|
||||
$field_name = $this->translation->hasField('content_translation_created') ? 'content_translation_created' : 'created';
|
||||
return $this->translation->get($field_name)->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setCreatedTime($timestamp) {
|
||||
$field_name = $this->translation->hasField('content_translation_created') ? 'content_translation_created' : 'created';
|
||||
$this->setFieldOnlyIfTranslatable($field_name, $timestamp);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getChangedTime() {
|
||||
return $this->translation->hasField('content_translation_changed') ? $this->translation->get('content_translation_changed')->value : $this->translation->getChangedTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setChangedTime($timestamp) {
|
||||
$field_name = $this->translation->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
|
||||
$this->setFieldOnlyIfTranslatable($field_name, $timestamp);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a field value, only if the field is translatable.
|
||||
*
|
||||
* @param string $field_name
|
||||
* The name of the field.
|
||||
* @param mixed $value
|
||||
* The field value to be set.
|
||||
*/
|
||||
protected function setFieldOnlyIfTranslatable($field_name, $value) {
|
||||
if ($this->translation->getFieldDefinition($field_name)->isTranslatable()) {
|
||||
$this->translation->set($field_name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\user\UserInterface;
|
||||
|
||||
/**
|
||||
* Common interface for content translation metadata wrappers.
|
||||
*
|
||||
* This acts as a wrapper for an entity translation object, encapsulating the
|
||||
* logic needed to retrieve translation metadata.
|
||||
*/
|
||||
interface ContentTranslationMetadataWrapperInterface {
|
||||
|
||||
/**
|
||||
* Retrieves the source language for this translation.
|
||||
*
|
||||
* @return string
|
||||
* The source language code.
|
||||
*/
|
||||
public function getSource();
|
||||
|
||||
/**
|
||||
* Sets the source language for this translation.
|
||||
*
|
||||
* @param string $source
|
||||
* The source language code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSource($source);
|
||||
|
||||
/**
|
||||
* Returns the translation outdated status.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the translation is outdated, FALSE otherwise.
|
||||
*/
|
||||
public function isOutdated();
|
||||
|
||||
/**
|
||||
* Sets the translation outdated status.
|
||||
*
|
||||
* @param bool $outdated
|
||||
* TRUE if the translation is outdated, FALSE otherwise.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setOutdated($outdated);
|
||||
|
||||
/**
|
||||
* Returns the translation author.
|
||||
*
|
||||
* @return \Drupal\user\UserInterface
|
||||
* The user entity for the translation author.
|
||||
*/
|
||||
public function getAuthor();
|
||||
|
||||
/**
|
||||
* Sets the translation author.
|
||||
*
|
||||
* The metadata field will be updated, only if it's translatable.
|
||||
*
|
||||
* @param \Drupal\user\UserInterface $account
|
||||
* The translation author user entity.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setAuthor(UserInterface $account);
|
||||
|
||||
/**
|
||||
* Returns the translation published status.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the translation is published, FALSE otherwise.
|
||||
*/
|
||||
public function isPublished();
|
||||
|
||||
/**
|
||||
* Sets the translation published status.
|
||||
*
|
||||
* The metadata field will be updated, only if it's translatable.
|
||||
*
|
||||
* @param bool $published
|
||||
* TRUE if the translation is published, FALSE otherwise.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublished($published);
|
||||
|
||||
/**
|
||||
* Returns the translation creation timestamp.
|
||||
*
|
||||
* @return int
|
||||
* The UNIX timestamp of when the translation was created.
|
||||
*/
|
||||
public function getCreatedTime();
|
||||
|
||||
/**
|
||||
* Sets the translation creation timestamp.
|
||||
*
|
||||
* The metadata field will be updated, only if it's translatable.
|
||||
*
|
||||
* @param int $timestamp
|
||||
* The UNIX timestamp of when the translation was created.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCreatedTime($timestamp);
|
||||
|
||||
/**
|
||||
* Returns the timestamp of the last entity change from current translation.
|
||||
*
|
||||
* @return int
|
||||
* The timestamp of the last entity save operation.
|
||||
*/
|
||||
public function getChangedTime();
|
||||
|
||||
/**
|
||||
* Sets the translation modification timestamp.
|
||||
*
|
||||
* The metadata field will be updated, only if it's translatable.
|
||||
*
|
||||
* @param int $timestamp
|
||||
* The UNIX timestamp of when the translation was last modified.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setChangedTime($timestamp);
|
||||
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides dynamic permissions for the content_translation module.
|
||||
*/
|
||||
class ContentTranslationPermissions implements ContainerInjectionInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity bundle info.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* Constructs a ContentTranslationPermissions instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
|
||||
* The content translation manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
|
||||
* The entity type bundle info.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ContentTranslationManagerInterface $content_translation_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->contentTranslationManager = $content_translation_manager;
|
||||
$this->entityTypeBundleInfo = $entity_type_bundle_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('content_translation.manager'),
|
||||
$container->get('entity_type.bundle.info')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of content translation permissions.
|
||||
*
|
||||
* @return array
|
||||
* An associative array of permissions keyed by permission name.
|
||||
*/
|
||||
public function contentPermissions() {
|
||||
$permissions = [];
|
||||
// Create a translate permission for each enabled entity type and
|
||||
// (optionally) bundle.
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
if ($permission_granularity = $entity_type->getPermissionGranularity()) {
|
||||
switch ($permission_granularity) {
|
||||
case 'bundle':
|
||||
foreach ($this->entityTypeBundleInfo->getBundleInfo($entity_type_id) as $bundle => $bundle_info) {
|
||||
if ($this->contentTranslationManager->isEnabled($entity_type_id, $bundle)) {
|
||||
$permissions["translate $bundle $entity_type_id"] = $this->buildBundlePermission($entity_type, $bundle, $bundle_info);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'entity_type':
|
||||
if ($this->contentTranslationManager->isEnabled($entity_type_id)) {
|
||||
$permissions["translate $entity_type_id"] = [
|
||||
'title' => $this->t('Translate @entity_label', ['@entity_label' => $entity_type->getSingularLabel()]),
|
||||
'dependencies' => ['module' => [$entity_type->getProvider()]],
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a content translation permission array for a bundle.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type.
|
||||
* @param string $bundle
|
||||
* The bundle to build the translation permission for.
|
||||
* @param array $bundle_info
|
||||
* The bundle info.
|
||||
*
|
||||
* @return array
|
||||
* The permission details, keyed by 'title' and 'dependencies'.
|
||||
*/
|
||||
private function buildBundlePermission(EntityTypeInterface $entity_type, string $bundle, array $bundle_info) {
|
||||
$permission = [
|
||||
'title' => $this->t('Translate %bundle_label @entity_label', [
|
||||
'@entity_label' => $entity_type->getSingularLabel(),
|
||||
'%bundle_label' => $bundle_info['label'] ?? $bundle,
|
||||
]),
|
||||
];
|
||||
|
||||
// If the entity type uses bundle entities, add a dependency on the bundle.
|
||||
$bundle_entity_type = $entity_type->getBundleEntityType();
|
||||
if ($bundle_entity_type && $bundle_entity = $this->entityTypeManager->getStorage($bundle_entity_type)->load($bundle)) {
|
||||
$permission['dependencies'][$bundle_entity->getConfigDependencyKey()][] = $bundle_entity->getConfigDependencyName();
|
||||
}
|
||||
else {
|
||||
$permission['dependencies']['module'][] = $entity_type->getProvider();
|
||||
}
|
||||
return $permission;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,449 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Controller;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Base class for entity translation controllers.
|
||||
*/
|
||||
class ContentTranslationController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Initializes a content translation controller.
|
||||
*
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $manager
|
||||
* A content translation manager instance.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
|
||||
* The entity field manager service.
|
||||
* @param \Drupal\Component\Datetime\TimeInterface $time
|
||||
* The time service.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ContentTranslationManagerInterface $manager,
|
||||
protected EntityFieldManagerInterface $entityFieldManager,
|
||||
protected TimeInterface $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 in
|
||||
// https://www.drupal.org/node/2006348. Maybe like
|
||||
// phpcs:ignore
|
||||
// $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
|
||||
|
||||
// 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 in
|
||||
// https://www.drupal.org/node/2006348. Maybe like
|
||||
// phpcs:ignore
|
||||
// operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* Provides field translation synchronization capabilities.
|
||||
*/
|
||||
class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The field type plugin manager.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
|
||||
*/
|
||||
protected $fieldTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a FieldTranslationSynchronizer object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
|
||||
* The field type plugin manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->fieldTypeManager = $field_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition) {
|
||||
$properties = [];
|
||||
$settings = $this->getFieldSynchronizationSettings($field_definition);
|
||||
foreach ($settings as $group => $translatable) {
|
||||
if (!$translatable) {
|
||||
$field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType());
|
||||
if (!empty($field_type_definition['column_groups'][$group]['columns'])) {
|
||||
$properties = array_merge($properties, $field_type_definition['column_groups'][$group]['columns']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the synchronization settings for the specified field.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* A field definition.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of synchronized field property names.
|
||||
*/
|
||||
protected function getFieldSynchronizationSettings(FieldDefinitionInterface $field_definition) {
|
||||
if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable()) {
|
||||
return $field_definition->getThirdPartySetting('content_translation', 'translation_sync', []);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) {
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
|
||||
// If we have no information about what to sync to, if we are creating a new
|
||||
// entity, if we have no translations for the current entity and we are not
|
||||
// creating one, then there is nothing to synchronize.
|
||||
if (empty($sync_langcode) || $entity->isNew() || count($translations) < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the entity language is being changed there is nothing to synchronize.
|
||||
$entity_unchanged = $this->getOriginalEntity($entity);
|
||||
if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->isNewRevision()) {
|
||||
if ($entity->isDefaultTranslationAffectedOnly()) {
|
||||
// If changes to untranslatable fields are configured to affect only the
|
||||
// default translation, we need to skip synchronization in pending
|
||||
// revisions, otherwise multiple translations would be affected.
|
||||
if (!$entity->isDefaultRevision()) {
|
||||
return;
|
||||
}
|
||||
// When this mode is enabled, changes to synchronized properties are
|
||||
// allowed only in the default translation, thus we need to make sure
|
||||
// this is always used as source for the synchronization process.
|
||||
else {
|
||||
$sync_langcode = $entity->getUntranslated()->language()->getId();
|
||||
}
|
||||
}
|
||||
elseif ($entity->isDefaultRevision()) {
|
||||
// If a new default revision is being saved, but a newer default
|
||||
// revision was created meanwhile, use any other translation as source
|
||||
// for synchronization, since that will have been merged from the
|
||||
// default revision. In this case the actual language does not matter as
|
||||
// synchronized properties are the same for all the translations in the
|
||||
// default revision.
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
|
||||
$default_revision = $this->entityTypeManager
|
||||
->getStorage($entity->getEntityTypeId())
|
||||
->load($entity->id());
|
||||
if ($default_revision->getLoadedRevisionId() !== $entity->getLoadedRevisionId()) {
|
||||
$other_langcodes = array_diff_key($default_revision->getTranslationLanguages(), [$sync_langcode => FALSE]);
|
||||
if ($other_langcodes) {
|
||||
$sync_langcode = key($other_langcodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
|
||||
foreach ($entity as $field_name => $items) {
|
||||
$field_definition = $items->getFieldDefinition();
|
||||
$field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType());
|
||||
$column_groups = $field_type_definition['column_groups'];
|
||||
|
||||
// Sync if the field is translatable, not empty, and the synchronization
|
||||
// setting is enabled.
|
||||
if (($translation_sync = $this->getFieldSynchronizationSettings($field_definition)) && !$items->isEmpty()) {
|
||||
// Retrieve all the untranslatable column groups and merge them into
|
||||
// single list.
|
||||
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
|
||||
|
||||
// If a group was selected has the require_all_groups_for_translation
|
||||
// flag set, there are no untranslatable columns. This is done because
|
||||
// the UI adds JavaScript that disables the other checkboxes, so their
|
||||
// values are not saved.
|
||||
foreach (array_filter($translation_sync) as $group) {
|
||||
if (!empty($column_groups[$group]['require_all_groups_for_translation'])) {
|
||||
$groups = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!empty($groups)) {
|
||||
$columns = [];
|
||||
foreach ($groups as $group) {
|
||||
$info = $column_groups[$group];
|
||||
// A missing 'columns' key indicates we have a single-column group.
|
||||
$columns = array_merge($columns, $info['columns'] ?? [$group]);
|
||||
}
|
||||
if (!empty($columns)) {
|
||||
$values = [];
|
||||
foreach ($translations as $langcode => $language) {
|
||||
$values[$langcode] = $entity->getTranslation($langcode)->get($field_name)->getValue();
|
||||
}
|
||||
|
||||
// If a translation is being created, the original values should be
|
||||
// used as the unchanged items. In fact there are no unchanged items
|
||||
// to check against.
|
||||
$langcode = $original_langcode ?: $sync_langcode;
|
||||
$unchanged_items = $entity_unchanged->getTranslation($langcode)->get($field_name)->getValue();
|
||||
$this->synchronizeItems($values, $unchanged_items, $sync_langcode, array_keys($translations), $columns);
|
||||
|
||||
foreach ($translations as $langcode => $language) {
|
||||
$entity->getTranslation($langcode)->get($field_name)->setValue($values[$langcode]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original unchanged entity to be used to detect changes.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being changed.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* The unchanged entity.
|
||||
*/
|
||||
protected function getOriginalEntity(ContentEntityInterface $entity) {
|
||||
if (!$entity->getOriginal()) {
|
||||
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
|
||||
$original = $entity->wasDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
|
||||
}
|
||||
else {
|
||||
$original = $entity->getOriginal();
|
||||
}
|
||||
return $original;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $properties) {
|
||||
$source_items = $values[$sync_langcode];
|
||||
|
||||
// Make sure we can detect any change in the source items.
|
||||
$change_map = [];
|
||||
|
||||
// By picking the maximum size between updated and unchanged items, we make
|
||||
// sure to process also removed items.
|
||||
$total = max([count($source_items), count($unchanged_items)]);
|
||||
|
||||
// As a first step we build a map of the deltas corresponding to the column
|
||||
// values to be synchronized. Recording both the old values and the new
|
||||
// values will allow us to detect any change in the order of the new items
|
||||
// for each column.
|
||||
for ($delta = 0; $delta < $total; $delta++) {
|
||||
foreach (['old' => $unchanged_items, 'new' => $source_items] as $key => $items) {
|
||||
if ($item_id = $this->itemHash($items, $delta, $properties)) {
|
||||
$change_map[$item_id][$key][] = $delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backup field values and the change map.
|
||||
$original_field_values = $values;
|
||||
$original_change_map = $change_map;
|
||||
|
||||
// Reset field values so that no spurious one is stored. Source values must
|
||||
// be preserved in any case.
|
||||
$values = [$sync_langcode => $source_items];
|
||||
|
||||
// Update field translations.
|
||||
foreach ($translations as $langcode) {
|
||||
|
||||
// We need to synchronize only values different from the source ones.
|
||||
if ($langcode != $sync_langcode) {
|
||||
// Reinitialize the change map as it is emptied while processing each
|
||||
// language.
|
||||
$change_map = $original_change_map;
|
||||
|
||||
// By using the maximum cardinality we ensure to process removed items.
|
||||
for ($delta = 0; $delta < $total; $delta++) {
|
||||
// By inspecting the map we built before we can tell whether a value
|
||||
// has been created or removed. A changed value will be interpreted as
|
||||
// a new value, in fact it did not exist before.
|
||||
$created = TRUE;
|
||||
$removed = TRUE;
|
||||
$old_delta = NULL;
|
||||
$new_delta = NULL;
|
||||
|
||||
if ($item_id = $this->itemHash($source_items, $delta, $properties)) {
|
||||
if (!empty($change_map[$item_id]['old'])) {
|
||||
$old_delta = array_shift($change_map[$item_id]['old']);
|
||||
}
|
||||
if (!empty($change_map[$item_id]['new'])) {
|
||||
$new_delta = array_shift($change_map[$item_id]['new']);
|
||||
}
|
||||
$created = $created && !isset($old_delta);
|
||||
$removed = $removed && !isset($new_delta);
|
||||
}
|
||||
|
||||
// If an item has been removed we do not store its translations.
|
||||
if ($removed) {
|
||||
continue;
|
||||
}
|
||||
// If a synchronized column has changed or has been created from
|
||||
// scratch we need to replace the values for this language as a
|
||||
// combination of the values that need to be synced from the source
|
||||
// items and the other columns from the existing values. This only
|
||||
// works if the delta exists in the language.
|
||||
elseif ($created && !empty($original_field_values[$langcode][$delta])) {
|
||||
$values[$langcode][$delta] = $this->createMergedItem($source_items[$delta], $original_field_values[$langcode][$delta], $properties);
|
||||
}
|
||||
// If the delta doesn't exist, copy from the source language.
|
||||
elseif ($created) {
|
||||
$values[$langcode][$delta] = $source_items[$delta];
|
||||
}
|
||||
// Otherwise the current item might have been reordered.
|
||||
elseif (isset($old_delta) && isset($new_delta)) {
|
||||
// If for any reason the old value is not defined for the current
|
||||
// language we fall back to the new source value, this way we ensure
|
||||
// the new values are at least propagated to all the translations.
|
||||
// If the value has only been reordered we just move the old one in
|
||||
// the new position.
|
||||
$item = $original_field_values[$langcode][$old_delta] ?? $source_items[$new_delta];
|
||||
// When saving a default revision starting from a pending revision,
|
||||
// we may have desynchronized field values, so we make sure that
|
||||
// untranslatable properties are synchronized, even if in any other
|
||||
// situation this would not be necessary.
|
||||
$values[$langcode][$new_delta] = $this->createMergedItem($source_items[$new_delta], $item, $properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a merged item.
|
||||
*
|
||||
* @param array $source_item
|
||||
* An item containing the untranslatable properties to be synchronized.
|
||||
* @param array $target_item
|
||||
* An item containing the translatable properties to be kept.
|
||||
* @param string[] $properties
|
||||
* An array of properties to be synchronized.
|
||||
*
|
||||
* @return array
|
||||
* A merged item array.
|
||||
*/
|
||||
protected function createMergedItem(array $source_item, array $target_item, array $properties) {
|
||||
$property_keys = array_flip($properties);
|
||||
$item_properties_to_sync = array_intersect_key($source_item, $property_keys);
|
||||
$item_properties_to_keep = array_diff_key($target_item, $property_keys);
|
||||
return $item_properties_to_sync + $item_properties_to_keep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash code for the specified item.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of field items.
|
||||
* @param int $delta
|
||||
* The delta identifying the item to be processed.
|
||||
* @param array $properties
|
||||
* An array of column names to be synchronized.
|
||||
*
|
||||
* @return string
|
||||
* A hash code that can be used to identify the item.
|
||||
*/
|
||||
protected function itemHash(array $items, $delta, array $properties) {
|
||||
$values = [];
|
||||
|
||||
if (isset($items[$delta])) {
|
||||
foreach ($properties as $property) {
|
||||
if (!empty($items[$delta][$property])) {
|
||||
$value = $items[$delta][$property];
|
||||
// String and integer values are by far the most common item values,
|
||||
// thus we special-case them to improve performance.
|
||||
$values[] = is_string($value) || is_int($value) ? $value : hash('sha256', serialize($value));
|
||||
}
|
||||
else {
|
||||
// Explicitly track also empty values.
|
||||
$values[] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode('.', $values);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
|
||||
/**
|
||||
* Provides field translation synchronization capabilities.
|
||||
*/
|
||||
interface FieldTranslationSynchronizerInterface {
|
||||
|
||||
/**
|
||||
* Performs field column synchronization on the given entity.
|
||||
*
|
||||
* Field column synchronization takes care of propagating any change in the
|
||||
* field items order and in the column values themselves to all the available
|
||||
* translations. This functionality is provided by defining a
|
||||
* 'translation_sync' key for the 'content_translation' module's portion of
|
||||
* the field definition's 'third_party_settings', holding an array of
|
||||
* column names to be synchronized. The synchronized column values are shared
|
||||
* across translations, while the rest varies per-language. This is useful for
|
||||
* instance to translate the "alt" and "title" textual elements of an image
|
||||
* field, while keeping the same image on every translation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity whose values should be synchronized.
|
||||
* @param string $sync_langcode
|
||||
* The language of the translation whose values should be used as source for
|
||||
* synchronization.
|
||||
* @param string $original_langcode
|
||||
* (optional) If a new translation is being created, this should be the
|
||||
* language code of the original values. Defaults to NULL.
|
||||
*/
|
||||
public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL);
|
||||
|
||||
/**
|
||||
* Synchronize the items of a single field.
|
||||
*
|
||||
* All the column values of the "active" language are compared to the
|
||||
* unchanged values to detect any addition, removal or change in the items
|
||||
* order. Subsequently the detected changes are performed on the field items
|
||||
* in other available languages.
|
||||
*
|
||||
* @param array $field_values
|
||||
* The field values to be synchronized.
|
||||
* @param array $unchanged_items
|
||||
* The unchanged items to be used to detect changes.
|
||||
* @param string $sync_langcode
|
||||
* The language code of the items to use as source values.
|
||||
* @param array $translations
|
||||
* An array of all the available language codes for the given field.
|
||||
* @param array $properties
|
||||
* An array of property names to be synchronized.
|
||||
*/
|
||||
public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $properties);
|
||||
|
||||
/**
|
||||
* Returns the synchronized properties for the specified field definition.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* A field definition.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of synchronized field property names.
|
||||
*/
|
||||
public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition);
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Form;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityDeleteForm;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
/**
|
||||
* Delete translation form for content_translation module.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ContentTranslationDeleteForm extends ContentEntityDeleteForm {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'content_translation_delete_confirm';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, ?LanguageInterface $language = NULL) {
|
||||
if ($language) {
|
||||
$form_state->set('langcode', $language->getId());
|
||||
}
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,558 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Hook;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityFormInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\content_translation\BundleTranslationSettingsInterface;
|
||||
use Drupal\language\ContentLanguageSettingsInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
use Drupal\Core\Hook\Order\Order;
|
||||
|
||||
/**
|
||||
* Hook implementations for content_translation.
|
||||
*/
|
||||
class ContentTranslationHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
#[Hook('help')]
|
||||
public function help($route_name, RouteMatchInterface $route_match): ?string {
|
||||
switch ($route_name) {
|
||||
case 'help.page.content_translation':
|
||||
$output = '';
|
||||
$output .= '<h2>' . $this->t('About') . '</h2>';
|
||||
$output .= '<p>' . $this->t('The Content Translation module allows you to translate content, comments, content blocks, taxonomy terms, users and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a>. Together with the modules <a href=":language">Language</a>, <a href=":config-trans">Configuration Translation</a>, and <a href=":locale">Interface Translation</a>, it allows you to build multilingual websites. For more information, see the <a href=":translation-entity">online documentation for the Content Translation module</a>.', [
|
||||
':locale' => \Drupal::moduleHandler()->moduleExists('locale') ? Url::fromRoute('help.page', [
|
||||
'name' => 'locale',
|
||||
])->toString() : '#',
|
||||
':config-trans' => \Drupal::moduleHandler()->moduleExists('config_translation') ? Url::fromRoute('help.page', [
|
||||
'name' => 'config_translation',
|
||||
])->toString() : '#',
|
||||
':language' => Url::fromRoute('help.page', [
|
||||
'name' => 'language',
|
||||
])->toString(),
|
||||
':translation-entity' => 'https://www.drupal.org/docs/8/core/modules/content-translation',
|
||||
':field_help' => Url::fromRoute('help.page', [
|
||||
'name' => 'field',
|
||||
])->toString(),
|
||||
]) . '</p>';
|
||||
$output .= '<h2>' . $this->t('Uses') . '</h2>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . $this->t('Enabling translation') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('In order to translate content, the website must have at least two <a href=":url">languages</a>. When that is the case, you can enable translation for the desired content entities on the <a href=":translation-entity">Content language</a> page. When enabling translation you can choose the default language for content and decide whether to show the language selection field on the content editing forms.', [
|
||||
':url' => Url::fromRoute('entity.configurable_language.collection')->toString(),
|
||||
':translation-entity' => Url::fromRoute('language.content_settings_page')->toString(),
|
||||
':language-help' => Url::fromRoute('help.page', [
|
||||
'name' => 'language',
|
||||
])->toString(),
|
||||
]) . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Enabling field translation') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('You can define which fields of a content entity can be translated. For example, you might want to translate the title and body field while leaving the image field untranslated. If you exclude a field from being translated, it will still show up in the content editing form, but any changes made to that field will be applied to <em>all</em> translations of that content.') . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Translating content') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('If translation is enabled you can translate a content entity via the Translate tab (or Translate link). The Translations page of a content entity gives an overview of the translation status for the current content and lets you add, edit, and delete its translations. This process is similar for every translatable content entity on your site.') . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Changing the source language for a translation') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('When you add a new translation, the original text you are translating is displayed in the edit form as the <em>source</em>. If at least one translation of the original content already exists when you add a new translation, you can choose either the original content (default) or one of the other translations as the source, using the select list in the Source language section. After saving the translation, the chosen source language is then listed on the Translate tab of the content.') . '</dd>';
|
||||
$output .= '<dt>' . $this->t('Setting status of translations') . '</dt>';
|
||||
$output .= '<dd>' . $this->t('If you edit a translation in one language you may want to set the status of the other translations as <em>out-of-date</em>. You can set this status by selecting the <em>Flag other translations as outdated</em> checkbox in the Translation section of the content editing form. The status will be visible on the Translations page.') . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
|
||||
case 'language.content_settings_page':
|
||||
$output = '';
|
||||
if (!\Drupal::languageManager()->isMultilingual()) {
|
||||
$output .= '<p>' . $this->t('Before you can translate content, there must be at least two languages added on the <a href=":url">languages administration</a> page.', [
|
||||
':url' => Url::fromRoute('entity.configurable_language.collection')->toString(),
|
||||
]) . '</p>';
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_language_types_info_alter().
|
||||
*/
|
||||
#[Hook('language_types_info_alter')]
|
||||
public function languageTypesInfoAlter(array &$language_types): void {
|
||||
// Make content language negotiation configurable by removing the 'locked'
|
||||
// flag.
|
||||
$language_types[LanguageInterface::TYPE_CONTENT]['locked'] = FALSE;
|
||||
unset($language_types[LanguageInterface::TYPE_CONTENT]['fixed']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_type_alter().
|
||||
*
|
||||
* The content translation UI relies on the entity info to provide its
|
||||
* features. See the documentation of hook_entity_type_build() in the Entity
|
||||
* API documentation for more details on all the entity info keys that may be
|
||||
* defined.
|
||||
*
|
||||
* To make Content Translation automatically support an entity type some keys
|
||||
* may need to be defined, but none of them is required unless the entity path
|
||||
* is different from the usual /ENTITY_TYPE/{ENTITY_TYPE} pattern (for
|
||||
* instance "/taxonomy/term/{taxonomy_term}"). Here are a list of those
|
||||
* optional keys:
|
||||
* - canonical: This key (in the 'links' entity info property) must be defined
|
||||
* if the entity path is different from /ENTITY_TYPE/{ENTITY_TYPE}
|
||||
* - translation: This key (in the 'handlers' entity annotation property)
|
||||
* specifies the translation handler for the entity type. If an entity type
|
||||
* is translatable and no translation handler is defined,
|
||||
* \Drupal\content_translation\ContentTranslationHandler will be assumed.
|
||||
* Every translation handler must implement
|
||||
* \Drupal\content_translation\ContentTranslationHandlerInterface.
|
||||
* - content_translation_ui_skip: By default, entity types that do not have a
|
||||
* canonical link template cannot be enabled for translation. Setting this
|
||||
* key to TRUE overrides that. When that key is set, the Content Translation
|
||||
* module will not provide any UI for translating the entity type, and the
|
||||
* entity type should implement its own UI. For instance, this is useful for
|
||||
* entity types that are embedded into others for editing (which would not
|
||||
* need a canonical link, but could still support translation).
|
||||
* - content_translation_metadata: To implement its business logic the content
|
||||
* translation UI relies on various metadata items describing the
|
||||
* translation state. The default implementation is provided by
|
||||
* \Drupal\content_translation\ContentTranslationMetadataWrapper, which is
|
||||
* relying on one field for each metadata item (field definitions are
|
||||
* provided by the translation handler). Entity types needing to customize
|
||||
* this behavior can specify an alternative class through the
|
||||
* 'content_translation_metadata' key in the entity type definition. Every
|
||||
* content translation metadata wrapper needs to implement
|
||||
* \Drupal\content_translation\ContentTranslationMetadataWrapperInterface.
|
||||
*
|
||||
* If the entity paths match the default pattern above and there is no need
|
||||
* for an entity-specific translation handler, Content Translation will
|
||||
* provide built-in support for the entity. However enabling translation for
|
||||
* each translatable bundle will be required.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\Annotation\EntityType
|
||||
*/
|
||||
#[Hook('entity_type_alter', order: Order::Last)]
|
||||
public function entityTypeAlter(array &$entity_types) : void {
|
||||
// Provide defaults for translation info.
|
||||
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
|
||||
foreach ($entity_types as $entity_type) {
|
||||
if ($entity_type->isTranslatable()) {
|
||||
if (!$entity_type->hasHandlerClass('translation')) {
|
||||
$entity_type->setHandlerClass('translation', 'Drupal\content_translation\ContentTranslationHandler');
|
||||
}
|
||||
if (!$entity_type->get('content_translation_metadata')) {
|
||||
$entity_type->set('content_translation_metadata', 'Drupal\content_translation\ContentTranslationMetadataWrapper');
|
||||
}
|
||||
if (!$entity_type->getFormClass('content_translation_deletion')) {
|
||||
$entity_type->setFormClass('content_translation_deletion', '\Drupal\content_translation\Form\ContentTranslationDeleteForm');
|
||||
}
|
||||
$translation = $entity_type->get('translation');
|
||||
if (!$translation || !isset($translation['content_translation'])) {
|
||||
$translation['content_translation'] = [];
|
||||
}
|
||||
if ($entity_type->hasLinkTemplate('canonical')) {
|
||||
// Provide default route names for the translation paths.
|
||||
if (!$entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
|
||||
$translations_path = $entity_type->getLinkTemplate('canonical') . '/translations';
|
||||
$entity_type->setLinkTemplate('drupal:content-translation-overview', $translations_path);
|
||||
$entity_type->setLinkTemplate('drupal:content-translation-add', $translations_path . '/add/{source}/{target}');
|
||||
$entity_type->setLinkTemplate('drupal:content-translation-edit', $translations_path . '/edit/{language}');
|
||||
$entity_type->setLinkTemplate('drupal:content-translation-delete', $translations_path . '/delete/{language}');
|
||||
}
|
||||
// @todo Remove this as soon as menu access checks rely on the
|
||||
// controller. See https://www.drupal.org/node/2155787.
|
||||
$translation['content_translation'] += ['access_callback' => 'content_translation_translate_access'];
|
||||
}
|
||||
$entity_type->set('translation', $translation);
|
||||
}
|
||||
$entity_type->addConstraint('ContentTranslationSynchronizedFields');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_insert().
|
||||
*
|
||||
* Installs Content Translation's field storage definitions for the target
|
||||
* entity type, if required.
|
||||
*
|
||||
* Also clears the bundle information cache so that the bundle's
|
||||
* translatability will be set properly.
|
||||
*
|
||||
* @see content_translation_entity_bundle_info_alter()
|
||||
* @see \Drupal\content_translation\ContentTranslationManager::isEnabled()
|
||||
*/
|
||||
#[Hook('language_content_settings_insert')]
|
||||
public function languageContentSettingsInsert(ContentLanguageSettingsInterface $settings): void {
|
||||
if ($settings->getThirdPartySetting('content_translation', 'enabled', FALSE)) {
|
||||
_content_translation_install_field_storage_definitions($settings->getTargetEntityTypeId());
|
||||
}
|
||||
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_update().
|
||||
*
|
||||
* Installs Content Translation's field storage definitions for the target
|
||||
* entity type, if required.
|
||||
*
|
||||
* Also clears the bundle information cache so that the bundle's
|
||||
* translatability will be changed properly.
|
||||
*
|
||||
* @see content_translation_entity_bundle_info_alter()
|
||||
* @see \Drupal\content_translation\ContentTranslationManager::isEnabled()
|
||||
*/
|
||||
#[Hook('language_content_settings_update')]
|
||||
public function languageContentSettingsUpdate(ContentLanguageSettingsInterface $settings): void {
|
||||
$original_settings = $settings->getOriginal();
|
||||
if ($settings->getThirdPartySetting('content_translation', 'enabled', FALSE) && !$original_settings->getThirdPartySetting('content_translation', 'enabled', FALSE)) {
|
||||
_content_translation_install_field_storage_definitions($settings->getTargetEntityTypeId());
|
||||
}
|
||||
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_bundle_info_alter().
|
||||
*/
|
||||
#[Hook('entity_bundle_info_alter', order: Order::First)]
|
||||
public function entityBundleInfoAlter(&$bundles): void {
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
|
||||
$content_translation_manager = \Drupal::service('content_translation.manager');
|
||||
foreach ($bundles as $entity_type_id => &$info) {
|
||||
foreach ($info as $bundle => &$bundle_info) {
|
||||
$bundle_info['translatable'] = $content_translation_manager->isEnabled($entity_type_id, $bundle);
|
||||
if ($bundle_info['translatable'] && $content_translation_manager instanceof BundleTranslationSettingsInterface) {
|
||||
$settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
|
||||
// If pending revision support is enabled for this bundle, we need to
|
||||
// hide untranslatable field widgets, otherwise changes in pending
|
||||
// revisions might be overridden by changes in later default
|
||||
// revisions.
|
||||
$bundle_info['untranslatable_fields.default_translation_affected'] = !empty($settings['untranslatable_fields_hide']) || ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_base_field_info().
|
||||
*/
|
||||
#[Hook('entity_base_field_info')]
|
||||
public function entityBaseFieldInfo(EntityTypeInterface $entity_type): array {
|
||||
$info = [];
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
|
||||
$manager = \Drupal::service('content_translation.manager');
|
||||
$entity_type_id = $entity_type->id();
|
||||
if ($manager->isSupported($entity_type_id)) {
|
||||
$definitions = $manager->getTranslationHandler($entity_type_id)->getFieldDefinitions();
|
||||
$installed_storage_definitions = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions($entity_type_id);
|
||||
// We return metadata storage fields whenever content translation is
|
||||
// enabled or it was enabled before, so that we keep translation metadata
|
||||
// around when translation is disabled.
|
||||
// @todo Re-evaluate this approach and consider removing field storage
|
||||
// definitions and the related field data if the entity type has no
|
||||
// bundle enabled for translation. See https://www.drupal.org/i/2907777
|
||||
if ($manager->isEnabled($entity_type_id) || array_intersect_key($definitions, $installed_storage_definitions)) {
|
||||
$info = $definitions;
|
||||
}
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_field_info_alter().
|
||||
*
|
||||
* Content translation extends the @FieldType annotation with following key:
|
||||
* - column_groups: contains information about the field type properties
|
||||
* which columns should be synchronized across different translations and
|
||||
* which are translatable. This is useful for instance to translate the
|
||||
* "alt" and "title" textual elements of an image field, while keeping the
|
||||
* same image on every translation. Each group has the following keys:
|
||||
* - title: Title of the column group.
|
||||
* - translatable: (optional) If the column group should be translatable by
|
||||
* default, defaults to FALSE.
|
||||
* - columns: (optional) A list of columns of this group. Defaults to the
|
||||
* name of the group as the single column.
|
||||
* - require_all_groups_for_translation: (optional) Set to TRUE to enforce
|
||||
* that making this column group translatable requires all others to be
|
||||
* translatable too.
|
||||
*
|
||||
* @see Drupal\image\Plugin\Field\FieldType\ImageItem
|
||||
*/
|
||||
#[Hook('field_info_alter')]
|
||||
public function fieldInfoAlter(&$info): void {
|
||||
foreach ($info as $key => $settings) {
|
||||
// Supply the column_groups key if it's not there.
|
||||
if (empty($settings['column_groups'])) {
|
||||
$info[$key]['column_groups'] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_operation().
|
||||
*/
|
||||
#[Hook('entity_operation')]
|
||||
public function entityOperation(EntityInterface $entity): array {
|
||||
$operations = [];
|
||||
if ($entity->hasLinkTemplate('drupal:content-translation-overview') && content_translation_translate_access($entity)->isAllowed()) {
|
||||
$operations['translate'] = [
|
||||
'title' => $this->t('Translate'),
|
||||
'url' => $entity->toUrl('drupal:content-translation-overview'),
|
||||
'weight' => 50,
|
||||
];
|
||||
}
|
||||
return $operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_views_data_alter().
|
||||
*/
|
||||
#[Hook('views_data_alter')]
|
||||
public function viewsDataAlter(array &$data): void {
|
||||
// Add the content translation entity link definition to Views data for
|
||||
// entity types having translation enabled.
|
||||
$entity_types = \Drupal::entityTypeManager()->getDefinitions();
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
|
||||
$manager = \Drupal::service('content_translation.manager');
|
||||
foreach ($entity_types as $entity_type_id => $entity_type) {
|
||||
$base_table = $entity_type->getBaseTable();
|
||||
if (isset($data[$base_table]) && $entity_type->hasLinkTemplate('drupal:content-translation-overview') && $manager->isEnabled($entity_type_id)) {
|
||||
$t_arguments = ['@entity_type_label' => $entity_type->getLabel()];
|
||||
$data[$base_table]['translation_link'] = [
|
||||
'field' => [
|
||||
'title' => $this->t('Link to translate @entity_type_label', $t_arguments),
|
||||
'help' => $this->t('Provide a translation link to the @entity_type_label.', $t_arguments),
|
||||
'id' => 'content_translation_link',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_menu_links_discovered_alter().
|
||||
*/
|
||||
#[Hook('menu_links_discovered_alter')]
|
||||
public function menuLinksDiscoveredAlter(array &$links): void {
|
||||
// Clarify where translation settings are located.
|
||||
$links['language.content_settings_page']['title'] = new TranslatableMarkup('Content language and translation');
|
||||
$links['language.content_settings_page']['description'] = new TranslatableMarkup('Configure language and translation support for content.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_alter().
|
||||
*/
|
||||
#[Hook('form_alter')]
|
||||
public function formAlter(array &$form, FormStateInterface $form_state) : void {
|
||||
$form_object = $form_state->getFormObject();
|
||||
if (!$form_object instanceof ContentEntityFormInterface) {
|
||||
return;
|
||||
}
|
||||
$entity = $form_object->getEntity();
|
||||
$op = $form_object->getOperation();
|
||||
// Let the content translation handler alter the content entity form. This
|
||||
// can be the 'add' or 'edit' form. It also tries a 'default' form in case
|
||||
// neither of the aforementioned forms are defined.
|
||||
if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && count($entity->getTranslationLanguages()) > 1 && in_array($op, ['edit', 'add', 'default'], TRUE)) {
|
||||
$controller = \Drupal::entityTypeManager()->getHandler($entity->getEntityTypeId(), 'translation');
|
||||
$controller->entityFormAlter($form, $form_state, $entity);
|
||||
// @todo Move the following lines to the code generating the property form
|
||||
// elements once we have an official #multilingual FAPI key.
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
$form_langcode = $form_object->getFormLangcode($form_state);
|
||||
// Handle fields shared between translations when there is at least one
|
||||
// translation available or a new one is being created.
|
||||
if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) {
|
||||
foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
|
||||
// Allow the widget to define if it should be treated as multilingual
|
||||
// by respecting an already set #multilingual key.
|
||||
if (isset($form[$field_name]) && !isset($form[$field_name]['#multilingual'])) {
|
||||
$form[$field_name]['#multilingual'] = $definition->isTranslatable();
|
||||
}
|
||||
}
|
||||
}
|
||||
// The footer region, if defined, may contain multilingual widgets so we
|
||||
// need to always display it.
|
||||
if (isset($form['footer'])) {
|
||||
$form['footer']['#multilingual'] = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_language_fallback_candidates_OPERATION_alter().
|
||||
*
|
||||
* Performs language fallback for inaccessible translations.
|
||||
*/
|
||||
#[Hook('language_fallback_candidates_entity_view_alter')]
|
||||
public function languageFallbackCandidatesEntityViewAlter(&$candidates, $context): void {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $context['data'];
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
|
||||
$manager = \Drupal::service('content_translation.manager');
|
||||
if ($manager->isEnabled($entity_type_id, $entity->bundle())) {
|
||||
/** @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
|
||||
$handler = \Drupal::entityTypeManager()->getHandler($entity->getEntityTypeId(), 'translation');
|
||||
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
|
||||
$metadata = $manager->getTranslationMetadata($entity->getTranslation($langcode));
|
||||
if (!$metadata->isPublished()) {
|
||||
$access = $handler->getTranslationAccess($entity, 'update');
|
||||
$entity->addCacheableDependency($access);
|
||||
if (!$access->isAllowed()) {
|
||||
// If the user has no translation update access, also check view
|
||||
// access for that translation, to allow other modules to allow
|
||||
// access to unpublished translations.
|
||||
$access = $entity->getTranslation($langcode)->access('view', NULL, TRUE);
|
||||
$entity->addCacheableDependency($access);
|
||||
if (!$access->isAllowed()) {
|
||||
unset($candidates[$langcode]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_extra_field_info().
|
||||
*/
|
||||
#[Hook('entity_extra_field_info')]
|
||||
public function entityExtraFieldInfo(): array {
|
||||
$extra = [];
|
||||
$bundle_info_service = \Drupal::service('entity_type.bundle.info');
|
||||
foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type => $info) {
|
||||
foreach ($bundle_info_service->getBundleInfo($entity_type) as $bundle => $bundle_info) {
|
||||
if (\Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle)) {
|
||||
$extra[$entity_type][$bundle]['form']['translation'] = [
|
||||
'label' => $this->t('Translation'),
|
||||
'description' => $this->t('Translation settings'),
|
||||
'weight' => 10,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter() for 'field_config_edit_form'.
|
||||
*/
|
||||
#[Hook('form_field_config_edit_form_alter')]
|
||||
public function formFieldConfigEditFormAlter(array &$form, FormStateInterface $form_state) : void {
|
||||
$field = $form_state->getFormObject()->getEntity();
|
||||
$bundle_is_translatable = \Drupal::service('content_translation.manager')->isEnabled($field->getTargetEntityTypeId(), $field->getTargetBundle());
|
||||
$form['translatable'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Users may translate this field'),
|
||||
'#default_value' => $field->isTranslatable(),
|
||||
'#weight' => -1,
|
||||
'#disabled' => !$bundle_is_translatable,
|
||||
'#access' => $field->getFieldStorageDefinition()->isTranslatable(),
|
||||
];
|
||||
// Provide helpful pointers for administrators.
|
||||
if (\Drupal::currentUser()->hasPermission('administer content translation') && !$bundle_is_translatable) {
|
||||
$toggle_url = Url::fromRoute('language.content_settings_page', [], ['query' => \Drupal::destination()->getAsArray()])->toString();
|
||||
$form['translatable']['#description'] = $this->t('To configure translation for this field, <a href=":language-settings-url">enable language support</a> for this type.', [':language-settings-url' => $toggle_url]);
|
||||
}
|
||||
if ($field->isTranslatable()) {
|
||||
\Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin');
|
||||
$element = content_translation_field_sync_widget($field);
|
||||
if ($element) {
|
||||
$form['third_party_settings']['content_translation']['translation_sync'] = $element;
|
||||
$form['third_party_settings']['content_translation']['translation_sync']['#weight'] = -10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_presave().
|
||||
*/
|
||||
#[Hook('entity_presave')]
|
||||
public function entityPresave(EntityInterface $entity): void {
|
||||
if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && !$entity->isNew() && $entity->getOriginal()) {
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
|
||||
$manager = \Drupal::service('content_translation.manager');
|
||||
if (!$manager->isEnabled($entity->getEntityTypeId(), $entity->bundle())) {
|
||||
return;
|
||||
}
|
||||
$langcode = $entity->language()->getId();
|
||||
$source_langcode = !$entity->getOriginal()->hasTranslation($langcode) ? $manager->getTranslationMetadata($entity)->getSource() : NULL;
|
||||
\Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $langcode, $source_langcode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_element_info_alter().
|
||||
*/
|
||||
#[Hook('element_info_alter')]
|
||||
public function elementInfoAlter(&$type): void {
|
||||
if (isset($type['language_configuration'])) {
|
||||
$type['language_configuration']['#process'][] = 'content_translation_language_configuration_element_process';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter() for language_content_settings_form().
|
||||
*/
|
||||
#[Hook('form_language_content_settings_form_alter')]
|
||||
public function formLanguageContentSettingsFormAlter(array &$form, FormStateInterface $form_state) : void {
|
||||
\Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin');
|
||||
_content_translation_form_language_content_settings_form_alter($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_page_attachments().
|
||||
*/
|
||||
#[Hook('page_attachments')]
|
||||
public function pageAttachments(&$page): void {
|
||||
$cache = CacheableMetadata::createFromRenderArray($page);
|
||||
$route_match = \Drupal::routeMatch();
|
||||
// If the current route has no parameters, return.
|
||||
if (!($route = $route_match->getRouteObject()) || !($parameters = $route->getOption('parameters'))) {
|
||||
return;
|
||||
}
|
||||
$is_front = \Drupal::service('path.matcher')->isFrontPage();
|
||||
// Determine if the current route represents an entity.
|
||||
foreach ($parameters as $name => $options) {
|
||||
if (!isset($options['type']) || !str_starts_with($options['type'], 'entity:')) {
|
||||
continue;
|
||||
}
|
||||
$entity = $route_match->getParameter($name);
|
||||
if ($entity instanceof ContentEntityInterface && $entity->hasLinkTemplate('canonical')) {
|
||||
// Current route represents a content entity. Build hreflang links.
|
||||
foreach ($entity->getTranslationLanguages() as $language) {
|
||||
// Skip any translation that cannot be viewed.
|
||||
$translation = $entity->getTranslation($language->getId());
|
||||
$access = $translation->access('view', NULL, TRUE);
|
||||
$cache->addCacheableDependency($access);
|
||||
if (!$access->isAllowed()) {
|
||||
continue;
|
||||
}
|
||||
if ($is_front) {
|
||||
// If the current page is front page, do not create hreflang links
|
||||
// from the entity route, just add the languages to root path.
|
||||
$url = Url::fromRoute('<front>', [], ['absolute' => TRUE, 'language' => $language])->toString();
|
||||
}
|
||||
else {
|
||||
$url = $entity->toUrl('canonical')->setOption('language', $language)->setAbsolute()->toString();
|
||||
}
|
||||
$page['#attached']['html_head_link'][] = [['rel' => 'alternate', 'hreflang' => $language->getId(), 'href' => $url]];
|
||||
}
|
||||
}
|
||||
// Since entity was found, no need to iterate further.
|
||||
break;
|
||||
}
|
||||
// Apply updated caching information.
|
||||
$cache->applyTo($page);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides dynamic contextual links for content translation.
|
||||
*
|
||||
* @see \Drupal\content_translation\Plugin\Menu\ContextualLink\ContentTranslationContextualLinks
|
||||
*/
|
||||
class ContentTranslationContextualLinks extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* Constructs a new ContentTranslationContextualLinks.
|
||||
*
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
|
||||
* The content translation manager.
|
||||
*/
|
||||
public function __construct(ContentTranslationManagerInterface $content_translation_manager) {
|
||||
$this->contentTranslationManager = $content_translation_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('content_translation.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
// Create contextual links for translatable entity types.
|
||||
foreach ($this->contentTranslationManager->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
|
||||
$this->derivatives[$entity_type_id]['title'] = $this->t('Translate');
|
||||
$this->derivatives[$entity_type_id]['route_name'] = "entity.$entity_type_id.content_translation_overview";
|
||||
$this->derivatives[$entity_type_id]['group'] = $entity_type_id;
|
||||
}
|
||||
return parent::getDerivativeDefinitions($base_plugin_definition);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Plugin\Derivative;
|
||||
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
|
||||
/**
|
||||
* Provides dynamic local tasks for content translation.
|
||||
*/
|
||||
class ContentTranslationLocalTasks extends DeriverBase implements ContainerDeriverInterface {
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The base plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $basePluginId;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* Constructs a new ContentTranslationLocalTasks.
|
||||
*
|
||||
* @param string $base_plugin_id
|
||||
* The base plugin ID.
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
|
||||
* The content translation manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The translation manager.
|
||||
*/
|
||||
public function __construct($base_plugin_id, ContentTranslationManagerInterface $content_translation_manager, TranslationInterface $string_translation) {
|
||||
$this->basePluginId = $base_plugin_id;
|
||||
$this->contentTranslationManager = $content_translation_manager;
|
||||
$this->stringTranslation = $string_translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$base_plugin_id,
|
||||
$container->get('content_translation.manager'),
|
||||
$container->get('string_translation')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
// Create tabs for all possible entity types.
|
||||
foreach ($this->contentTranslationManager->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
|
||||
// Find the route name for the translation overview.
|
||||
$translation_route_name = "entity.$entity_type_id.content_translation_overview";
|
||||
|
||||
$base_route_name = "entity.$entity_type_id.canonical";
|
||||
$this->derivatives[$translation_route_name] = [
|
||||
'entity_type' => $entity_type_id,
|
||||
'title' => $this->t('Translate'),
|
||||
'route_name' => $translation_route_name,
|
||||
'base_route' => $base_route_name,
|
||||
] + $base_plugin_definition;
|
||||
}
|
||||
return parent::getDerivativeDefinitions($base_plugin_definition);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Validation\Attribute\Constraint;
|
||||
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
|
||||
|
||||
/**
|
||||
* Validation constraint for the entity changed timestamp.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[Constraint(
|
||||
id: 'ContentTranslationSynchronizedFields',
|
||||
label: new TranslatableMarkup('Content translation synchronized fields', [], ['context' => 'Validation']),
|
||||
type: ['entity']
|
||||
)]
|
||||
class ContentTranslationSynchronizedFieldsConstraint extends SymfonyConstraint {
|
||||
|
||||
/**
|
||||
* Message shown for non-translatable field changes in non-default revision.
|
||||
*
|
||||
* In this case "elements" refers to "field properties". It is what we are
|
||||
* using in the UI elsewhere.
|
||||
*/
|
||||
public string $defaultRevisionMessage = 'Non-translatable field elements can only be changed when updating the current revision.';
|
||||
|
||||
/**
|
||||
* Message shown for non-translatable field changes in different language.
|
||||
*
|
||||
* In this case "elements" refers to "field properties". It is what we are
|
||||
* using in the UI elsewhere.
|
||||
*/
|
||||
public string $defaultTranslationMessage = 'Non-translatable field elements can only be changed when updating the original language.';
|
||||
|
||||
}
|
||||
@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\content_translation\FieldTranslationSynchronizerInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Checks that synchronized fields are handled correctly in pending revisions.
|
||||
*
|
||||
* As for untranslatable fields, two modes are supported:
|
||||
* - When changes to untranslatable fields are configured to affect all revision
|
||||
* translations, synchronized field properties can be changed only in default
|
||||
* revisions.
|
||||
* - When changes to untranslatable fields affect are configured to affect only
|
||||
* the revision's default translation, synchronized field properties can be
|
||||
* changed only when editing the default translation. This may lead to
|
||||
* temporarily desynchronized values, when saving a pending revision for the
|
||||
* default translation that changes a synchronized property. These are
|
||||
* actually synchronized when saving changes to the default translation as a
|
||||
* new default revision.
|
||||
*
|
||||
* @see \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint
|
||||
* @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraintValidator
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ContentTranslationSynchronizedFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* The field translation synchronizer.
|
||||
*
|
||||
* @var \Drupal\content_translation\FieldTranslationSynchronizerInterface
|
||||
*/
|
||||
protected $synchronizer;
|
||||
|
||||
/**
|
||||
* ContentTranslationSynchronizedFieldsConstraintValidator constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
|
||||
* The content translation manager.
|
||||
* @param \Drupal\content_translation\FieldTranslationSynchronizerInterface $synchronizer
|
||||
* The field translation synchronizer.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ContentTranslationManagerInterface $content_translation_manager, FieldTranslationSynchronizerInterface $synchronizer) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->contentTranslationManager = $content_translation_manager;
|
||||
$this->synchronizer = $synchronizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('content_translation.manager'),
|
||||
$container->get('content_translation.synchronizer')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint): void {
|
||||
/** @var \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint $constraint */
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $value;
|
||||
if ($entity->isNew() || !$entity->getEntityType()->isRevisionable()) {
|
||||
return;
|
||||
}
|
||||
// When changes to untranslatable fields are configured to affect all
|
||||
// revision translations, we always allow changes in default revisions.
|
||||
if ($entity->isDefaultRevision() && !$entity->isDefaultTranslationAffectedOnly()) {
|
||||
return;
|
||||
}
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
|
||||
return;
|
||||
}
|
||||
$synchronized_properties = $this->getSynchronizedPropertiesByField($entity->getFieldDefinitions());
|
||||
if (!$synchronized_properties) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $original */
|
||||
$original = $this->getOriginalEntity($entity);
|
||||
$original_translation = $this->getOriginalTranslation($entity, $original);
|
||||
if ($this->hasSynchronizedPropertyChanges($entity, $original_translation, $synchronized_properties)) {
|
||||
if ($entity->isDefaultTranslationAffectedOnly()) {
|
||||
foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
|
||||
if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
|
||||
$this->context->addViolation($constraint->defaultTranslationMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->context->addViolation($constraint->defaultRevisionMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any synchronized property has changes.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being validated.
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $original
|
||||
* The original unchanged entity.
|
||||
* @param string[][] $synchronized_properties
|
||||
* An associative array of arrays of synchronized field properties keyed by
|
||||
* field name.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if changes in synchronized properties were detected, FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
protected function hasSynchronizedPropertyChanges(ContentEntityInterface $entity, ContentEntityInterface $original, array $synchronized_properties) {
|
||||
foreach ($synchronized_properties as $field_name => $properties) {
|
||||
foreach ($properties as $property) {
|
||||
$items = $entity->get($field_name)->getValue();
|
||||
$original_items = $original->get($field_name)->getValue();
|
||||
if (count($items) !== count($original_items)) {
|
||||
return TRUE;
|
||||
}
|
||||
foreach ($items as $delta => $item) {
|
||||
// @todo This loose comparison is not fully reliable. Revisit this
|
||||
// after https://www.drupal.org/project/drupal/issues/2941092.
|
||||
if ($items[$delta][$property] != $original_items[$delta][$property]) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original unchanged entity to be used to detect changes.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being changed.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* The unchanged entity.
|
||||
*/
|
||||
protected function getOriginalEntity(ContentEntityInterface $entity) {
|
||||
if (!$entity->getOriginal()) {
|
||||
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
|
||||
$original = $entity->wasDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
|
||||
}
|
||||
else {
|
||||
$original = $entity->getOriginal();
|
||||
}
|
||||
return $original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original translation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being validated.
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $original
|
||||
* The original entity.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* The original entity translation object.
|
||||
*/
|
||||
protected function getOriginalTranslation(ContentEntityInterface $entity, ContentEntityInterface $original) {
|
||||
// If the language of the default translation is changing, the original
|
||||
// translation will be the same as the original entity, but they won't
|
||||
// necessarily have the same langcode.
|
||||
if ($entity->isDefaultTranslation() && $original->language()->getId() !== $entity->language()->getId()) {
|
||||
return $original;
|
||||
}
|
||||
$langcode = $entity->language()->getId();
|
||||
if ($original->hasTranslation($langcode)) {
|
||||
$original_langcode = $langcode;
|
||||
}
|
||||
else {
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
|
||||
$original_langcode = $metadata->getSource();
|
||||
}
|
||||
return $original->getTranslation($original_langcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the synchronized properties for every specified field.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
|
||||
* An array of field definitions.
|
||||
*
|
||||
* @return string[][]
|
||||
* An associative array of arrays of field property names keyed by field
|
||||
* name.
|
||||
*/
|
||||
public function getSynchronizedPropertiesByField(array $field_definitions) {
|
||||
$synchronizer = $this->synchronizer;
|
||||
$synchronized_properties = array_filter(array_map(
|
||||
function (FieldDefinitionInterface $field_definition) use ($synchronizer) {
|
||||
return $synchronizer->getFieldSynchronizedProperties($field_definition);
|
||||
},
|
||||
$field_definitions
|
||||
));
|
||||
return $synchronized_properties;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Plugin\migrate\source\d7;
|
||||
|
||||
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
|
||||
|
||||
/**
|
||||
* Drupal 7 Entity Translation settings (variables) from database.
|
||||
*
|
||||
* For available configuration keys, refer to the parent classes.
|
||||
*
|
||||
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
|
||||
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
|
||||
*
|
||||
* @MigrateSource(
|
||||
* id = "d7_entity_translation_settings",
|
||||
* source_module = "entity_translation"
|
||||
* )
|
||||
*/
|
||||
class EntityTranslationSettings extends DrupalSqlBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
// Query all meaningful variables for entity translation.
|
||||
$query = $this->select('variable', 'v')
|
||||
->fields('v', ['name', 'value']);
|
||||
$condition = $query->orConditionGroup()
|
||||
// The 'entity_translation_entity_types' variable tells us which entity
|
||||
// type uses entity translation.
|
||||
->condition('name', 'entity_translation_entity_types')
|
||||
// The 'entity_translation_taxonomy' variable tells us which taxonomy
|
||||
// vocabulary uses entity_translation.
|
||||
->condition('name', 'entity_translation_taxonomy')
|
||||
// The 'entity_translation_settings_%' variables give us the entity
|
||||
// translation settings for each entity type and each bundle.
|
||||
->condition('name', 'entity_translation_settings_%', 'LIKE')
|
||||
// The 'language_content_type_%' variables tells us which node type and
|
||||
// which comment type uses entity translation.
|
||||
->condition('name', 'language_content_type_%', 'LIKE');
|
||||
$query->condition($condition);
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function initializeIterator() {
|
||||
$results = array_map('unserialize', $this->prepareQuery()->execute()->fetchAllKeyed());
|
||||
$rows = [];
|
||||
|
||||
// Find out which entity type uses entity translation by looking at the
|
||||
// 'entity_translation_entity_types' variable.
|
||||
$entity_types = array_filter($results['entity_translation_entity_types']);
|
||||
|
||||
// If no entity type uses entity translation, there's nothing to do.
|
||||
if (empty($entity_types)) {
|
||||
return new \ArrayIterator($rows);
|
||||
}
|
||||
|
||||
// Find out which node type uses entity translation by looking at the
|
||||
// 'language_content_type_%' variables.
|
||||
$node_types = [];
|
||||
foreach ($results as $name => $value) {
|
||||
if (preg_match('/^language_content_type_(.+)$/', $name, $matches) && (int) $value === 4) {
|
||||
$node_types[] = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Find out which vocabulary uses entity translation by looking at the
|
||||
// 'entity_translation_taxonomy' variable.
|
||||
$vocabularies = [];
|
||||
if (isset($results['entity_translation_taxonomy']) && is_array($results['entity_translation_taxonomy'])) {
|
||||
$vocabularies = array_keys(array_filter($results['entity_translation_taxonomy']));
|
||||
}
|
||||
|
||||
if (in_array('node', $entity_types, TRUE) && !empty($node_types)) {
|
||||
// For each node type that uses entity translation, check if a
|
||||
// settings variable exists for that node type, otherwise use default
|
||||
// values.
|
||||
foreach ($node_types as $node_type) {
|
||||
$settings = $results['entity_translation_settings_node__' . $node_type] ?? [];
|
||||
$rows[] = [
|
||||
'id' => 'node.' . $node_type,
|
||||
'target_entity_type_id' => 'node',
|
||||
'target_bundle' => $node_type,
|
||||
'default_langcode' => $settings['default_language'] ?? 'und',
|
||||
// The Drupal 7 'hide_language_selector' configuration has become
|
||||
// 'language_alterable' in Drupal 8 so we need to negate the value we
|
||||
// receive from the source. The Drupal 7 'hide_language_selector'
|
||||
// default value for the node entity type was FALSE so in Drupal 8 it
|
||||
// should be set to TRUE, unlike the other entity types for which
|
||||
// it's the opposite.
|
||||
'language_alterable' => isset($settings['hide_language_selector']) ? (bool) !$settings['hide_language_selector'] : TRUE,
|
||||
'untranslatable_fields_hide' => isset($settings['shared_fields_original_only']) ? (bool) $settings['shared_fields_original_only'] : FALSE,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('comment', $entity_types, TRUE) && !empty($node_types)) {
|
||||
// A comment type uses entity translation if the associated node type
|
||||
// uses it. So, for each node type that uses entity translation, check
|
||||
// if a settings variable exists for that comment type, otherwise use
|
||||
// default values.
|
||||
foreach ($node_types as $node_type) {
|
||||
$settings = $results['entity_translation_settings_comment__comment_node_' . $node_type] ?? [];
|
||||
// Forum uses a hardcoded comment type name, so make sure we use it
|
||||
// when we're dealing with forum comment type.
|
||||
$bundle = $node_type == 'forum' ? 'comment_forum' : 'comment_node_' . $node_type;
|
||||
$rows[] = [
|
||||
'id' => 'comment.' . $bundle,
|
||||
'target_entity_type_id' => 'comment',
|
||||
'target_bundle' => $bundle,
|
||||
'default_langcode' => $settings['default_language'] ?? 'xx-et-current',
|
||||
// The Drupal 7 'hide_language_selector' configuration has become
|
||||
// 'language_alterable' in Drupal 8 so we need to negate the value we
|
||||
// receive from the source. The Drupal 7 'hide_language_selector'
|
||||
// default value for the comment entity type was TRUE so in Drupal 8
|
||||
// it should be set to FALSE.
|
||||
'language_alterable' => isset($settings['hide_language_selector']) ? (bool) !$settings['hide_language_selector'] : FALSE,
|
||||
'untranslatable_fields_hide' => isset($settings['shared_fields_original_only']) ? (bool) $settings['shared_fields_original_only'] : FALSE,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('taxonomy_term', $entity_types, TRUE) && !empty($vocabularies)) {
|
||||
// For each vocabulary that uses entity translation, check if a
|
||||
// settings variable exists for that vocabulary, otherwise use default
|
||||
// values.
|
||||
foreach ($vocabularies as $vocabulary) {
|
||||
$settings = $results['entity_translation_settings_taxonomy_term__' . $vocabulary] ?? [];
|
||||
$rows[] = [
|
||||
'id' => 'taxonomy_term.' . $vocabulary,
|
||||
'target_entity_type_id' => 'taxonomy_term',
|
||||
'target_bundle' => $vocabulary,
|
||||
'default_langcode' => $settings['default_language'] ?? 'xx-et-default',
|
||||
// The Drupal 7 'hide_language_selector' configuration has become
|
||||
// 'language_alterable' in Drupal 8 so we need to negate the value we
|
||||
// receive from the source. The Drupal 7 'hide_language_selector'
|
||||
// default value for the taxonomy_term entity type was TRUE so in
|
||||
// Drupal 8 it should be set to FALSE.
|
||||
'language_alterable' => isset($settings['hide_language_selector']) ? (bool) !$settings['hide_language_selector'] : FALSE,
|
||||
'untranslatable_fields_hide' => isset($settings['shared_fields_original_only']) ? (bool) $settings['shared_fields_original_only'] : FALSE,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('user', $entity_types, TRUE)) {
|
||||
// User entity type is not bundleable. Check if a settings variable
|
||||
// exists, otherwise use default values.
|
||||
$settings = $results['entity_translation_settings_user__user'] ?? [];
|
||||
$rows[] = [
|
||||
'id' => 'user.user',
|
||||
'target_entity_type_id' => 'user',
|
||||
'target_bundle' => 'user',
|
||||
'default_langcode' => $settings['default_language'] ?? 'xx-et-default',
|
||||
// The Drupal 7 'hide_language_selector' configuration has become
|
||||
// 'language_alterable' in Drupal 8 so we need to negate the value we
|
||||
// receive from the source. The Drupal 7 'hide_language_selector'
|
||||
// default value for the user entity type was TRUE so in Drupal 8 it
|
||||
// should be set to FALSE.
|
||||
'language_alterable' => isset($settings['hide_language_selector']) ? (bool) !$settings['hide_language_selector'] : FALSE,
|
||||
'untranslatable_fields_hide' => isset($settings['shared_fields_original_only']) ? (bool) $settings['shared_fields_original_only'] : FALSE,
|
||||
];
|
||||
}
|
||||
|
||||
return new \ArrayIterator($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fields() {
|
||||
return [
|
||||
'id' => $this->t('The configuration ID'),
|
||||
'target_entity_type_id' => $this->t('The target entity type ID'),
|
||||
'target_bundle' => $this->t('The target bundle'),
|
||||
'default_langcode' => $this->t('The default language'),
|
||||
'language_alterable' => $this->t('Whether to show language selector on create and edit pages'),
|
||||
'untranslatable_fields_hide' => $this->t('Whether to hide non translatable fields on translation forms'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getIds() {
|
||||
$ids['id']['type'] = 'string';
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doCount() {
|
||||
// Since the number of variables we fetch with query() does not match the
|
||||
// actual number of rows generated by initializeIterator(), we need to
|
||||
// override count() to return the correct count.
|
||||
return (int) $this->initializeIterator()->count();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Plugin\views\field;
|
||||
|
||||
use Drupal\views\Attribute\ViewsField;
|
||||
use Drupal\views\Plugin\views\field\EntityLink;
|
||||
|
||||
/**
|
||||
* Provides a translation link for an entity.
|
||||
*
|
||||
* @ingroup views_field_handlers
|
||||
*/
|
||||
#[ViewsField("content_translation_link")]
|
||||
class TranslationLink extends EntityLink {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEntityLinkTemplate() {
|
||||
return 'drupal:content-translation-overview';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getDefaultLabel() {
|
||||
return $this->t('Translate');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Routing;
|
||||
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Core\Routing\RouteSubscriberBase;
|
||||
use Drupal\Core\Routing\RoutingEvents;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Subscriber for entity translation routes.
|
||||
*/
|
||||
class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* Constructs a ContentTranslationRouteSubscriber object.
|
||||
*
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
|
||||
* The content translation manager.
|
||||
*/
|
||||
public function __construct(ContentTranslationManagerInterface $content_translation_manager) {
|
||||
$this->contentTranslationManager = $content_translation_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function alterRoutes(RouteCollection $collection) {
|
||||
foreach ($this->contentTranslationManager->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
|
||||
// Inherit admin route status from edit route, if exists.
|
||||
$is_admin = FALSE;
|
||||
$route_name = "entity.$entity_type_id.edit_form";
|
||||
if ($edit_route = $collection->get($route_name)) {
|
||||
$is_admin = (bool) $edit_route->getOption('_admin_route');
|
||||
}
|
||||
|
||||
$load_latest_revision = ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id);
|
||||
|
||||
if ($entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
|
||||
$route = new Route(
|
||||
$entity_type->getLinkTemplate('drupal:content-translation-overview'),
|
||||
[
|
||||
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::overview',
|
||||
'entity_type_id' => $entity_type_id,
|
||||
],
|
||||
[
|
||||
'_entity_access' => $entity_type_id . '.view',
|
||||
'_access_content_translation_overview' => $entity_type_id,
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_latest_revision' => $load_latest_revision,
|
||||
],
|
||||
],
|
||||
'_admin_route' => $is_admin,
|
||||
]
|
||||
);
|
||||
$route_name = "entity.$entity_type_id.content_translation_overview";
|
||||
$collection->add($route_name, $route);
|
||||
}
|
||||
|
||||
if ($entity_type->hasLinkTemplate('drupal:content-translation-add')) {
|
||||
$route = new Route(
|
||||
$entity_type->getLinkTemplate('drupal:content-translation-add'),
|
||||
[
|
||||
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::add',
|
||||
'source' => NULL,
|
||||
'target' => NULL,
|
||||
'_title' => 'Add',
|
||||
'entity_type_id' => $entity_type_id,
|
||||
|
||||
],
|
||||
[
|
||||
'_entity_access' => $entity_type_id . '.view',
|
||||
'_access_content_translation_manage' => 'create',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'source' => [
|
||||
'type' => 'language',
|
||||
],
|
||||
'target' => [
|
||||
'type' => 'language',
|
||||
],
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_latest_revision' => $load_latest_revision,
|
||||
],
|
||||
],
|
||||
'_admin_route' => $is_admin,
|
||||
]
|
||||
);
|
||||
$collection->add("entity.$entity_type_id.content_translation_add", $route);
|
||||
}
|
||||
|
||||
if ($entity_type->hasLinkTemplate('drupal:content-translation-edit')) {
|
||||
$route = new Route(
|
||||
$entity_type->getLinkTemplate('drupal:content-translation-edit'),
|
||||
[
|
||||
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::edit',
|
||||
'language' => NULL,
|
||||
'_title' => 'Edit',
|
||||
'entity_type_id' => $entity_type_id,
|
||||
],
|
||||
[
|
||||
'_access_content_translation_manage' => 'update',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'language' => [
|
||||
'type' => 'language',
|
||||
],
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_latest_revision' => $load_latest_revision,
|
||||
],
|
||||
],
|
||||
'_admin_route' => $is_admin,
|
||||
]
|
||||
);
|
||||
$collection->add("entity.$entity_type_id.content_translation_edit", $route);
|
||||
}
|
||||
|
||||
if ($entity_type->hasLinkTemplate('drupal:content-translation-delete')) {
|
||||
$route = new Route(
|
||||
$entity_type->getLinkTemplate('drupal:content-translation-delete'),
|
||||
[
|
||||
'_entity_form' => $entity_type_id . '.content_translation_deletion',
|
||||
'language' => NULL,
|
||||
'_title' => 'Delete',
|
||||
'entity_type_id' => $entity_type_id,
|
||||
],
|
||||
[
|
||||
'_access_content_translation_manage' => 'delete',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'language' => [
|
||||
'type' => 'language',
|
||||
],
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_latest_revision' => $load_latest_revision,
|
||||
],
|
||||
],
|
||||
'_admin_route' => $is_admin,
|
||||
]
|
||||
);
|
||||
$collection->add("entity.$entity_type_id.content_translation_delete", $route);
|
||||
}
|
||||
|
||||
// Add our custom translation deletion access checker.
|
||||
if ($load_latest_revision) {
|
||||
$entity_delete_route = $collection->get("entity.$entity_type_id.delete_form");
|
||||
if ($entity_delete_route) {
|
||||
$entity_delete_route->addRequirements(['_access_content_translation_delete' => "$entity_type_id.delete"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents(): array {
|
||||
$events = parent::getSubscribedEvents();
|
||||
// Should run after AdminRouteSubscriber so the routes can inherit admin
|
||||
// status of the edit routes on entities. Therefore priority -210.
|
||||
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -210];
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
id: test
|
||||
label: null
|
||||
description: null
|
||||
@ -0,0 +1,17 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- entity_test.entity_test_bundle.test
|
||||
module:
|
||||
- content_translation
|
||||
third_party_settings:
|
||||
content_translation:
|
||||
enabled: true
|
||||
bundle_settings:
|
||||
untranslatable_fields_hide: '0'
|
||||
id: entity_test_with_bundle.test
|
||||
target_entity_type_id: entity_test_with_bundle
|
||||
target_bundle: test
|
||||
default_langcode: site_default
|
||||
language_alterable: true
|
||||
@ -0,0 +1,9 @@
|
||||
name: 'Content translation tests'
|
||||
type: module
|
||||
description: 'Provides content translation tests.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- drupal:content_translation
|
||||
- drupal:language
|
||||
- drupal:entity_test
|
||||
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\content_translation_test\Entity;
|
||||
|
||||
use Drupal\Core\Entity\Attribute\ContentEntityType;
|
||||
use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\entity_test\EntityTestForm;
|
||||
|
||||
/**
|
||||
* Defines the test entity class.
|
||||
*/
|
||||
#[ContentEntityType(
|
||||
id: 'entity_test_translatable_no_skip',
|
||||
label: new TranslatableMarkup('Test entity - Translatable check UI'),
|
||||
entity_keys: [
|
||||
'id' => 'id',
|
||||
'uuid' => 'uuid',
|
||||
'bundle' => 'type',
|
||||
'label' => 'name',
|
||||
'langcode' => 'langcode',
|
||||
],
|
||||
handlers: [
|
||||
'form' => ['default' => EntityTestForm::class],
|
||||
'route_provider' => [
|
||||
'html' => DefaultHtmlRouteProvider::class,
|
||||
],
|
||||
],
|
||||
links: [
|
||||
'edit-form' => '/entity_test_translatable_no_skip/{entity_test_translatable_no_skip}/edit',
|
||||
],
|
||||
admin_permission: 'administer entity_test content',
|
||||
base_table: 'entity_test_mul',
|
||||
data_table: 'entity_test_mul_property_data',
|
||||
translatable: TRUE,
|
||||
)]
|
||||
class EntityTestTranslatableNoUISkip extends EntityTest {
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\content_translation_test\Entity;
|
||||
|
||||
use Drupal\Core\Entity\Attribute\ContentEntityType;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
|
||||
/**
|
||||
* Defines the test entity class.
|
||||
*/
|
||||
#[ContentEntityType(
|
||||
id: 'entity_test_translatable_UI_skip',
|
||||
label: new TranslatableMarkup('Test entity - Translatable skip UI check'),
|
||||
entity_keys: [
|
||||
'id' => 'id',
|
||||
'uuid' => 'uuid',
|
||||
'bundle' => 'type',
|
||||
'label' => 'name',
|
||||
'langcode' => 'langcode',
|
||||
],
|
||||
base_table: 'entity_test_mul',
|
||||
data_table: 'entity_test_mul_property_data',
|
||||
translatable: TRUE,
|
||||
additional: [
|
||||
'content_translation_ui_skip' => TRUE,
|
||||
],
|
||||
)]
|
||||
class EntityTestTranslatableUISkip extends EntityTest {
|
||||
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\content_translation_test\Hook;
|
||||
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for content_translation_test.
|
||||
*/
|
||||
class ContentTranslationTestHooks {
|
||||
|
||||
/**
|
||||
* Implements hook_entity_bundle_info_alter().
|
||||
*/
|
||||
#[Hook('entity_bundle_info_alter')]
|
||||
public function entityBundleInfoAlter(&$bundles): void {
|
||||
// Store the initial status of the "translatable" property for the
|
||||
// "entity_test_mul" bundle.
|
||||
$translatable = !empty($bundles['entity_test_mul']['entity_test_mul']['translatable']);
|
||||
\Drupal::state()->set('content_translation_test.translatable', $translatable);
|
||||
// Make it translatable if Content Translation did not. This will make the
|
||||
// entity object translatable even if it is disabled in Content Translation
|
||||
// settings.
|
||||
if (!$translatable) {
|
||||
$bundles['entity_test_mul']['entity_test_mul']['translatable'] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_access().
|
||||
*/
|
||||
#[Hook('entity_access')]
|
||||
public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account): AccessResultInterface {
|
||||
$access = \Drupal::state()->get('content_translation.entity_access.' . $entity->getEntityTypeId());
|
||||
if (!empty($access[$operation])) {
|
||||
return AccessResult::allowed();
|
||||
}
|
||||
else {
|
||||
return AccessResult::neutral();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_BASE_FORM_ID_alter().
|
||||
*
|
||||
* Adds a textfield to node forms based on a request parameter.
|
||||
*/
|
||||
#[Hook('form_node_form_alter')]
|
||||
public function formNodeFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
|
||||
$langcode = $form_state->getFormObject()->getFormLangcode($form_state);
|
||||
if (in_array($langcode, ['en', 'fr']) && \Drupal::request()->get('test_field_only_en_fr')) {
|
||||
$form['test_field_only_en_fr'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => 'Field only available on the english and french form',
|
||||
];
|
||||
foreach (array_keys($form['actions']) as $action) {
|
||||
if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
|
||||
$form['actions'][$action]['#submit'][] = [$this, 'formNodeFormSubmit'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_translation_delete().
|
||||
*/
|
||||
#[Hook('entity_translation_delete')]
|
||||
public function entityTranslationDelete(EntityInterface $translation): void {
|
||||
\Drupal::state()->set('content_translation_test.translation_deleted', TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for custom field added based on a request parameter.
|
||||
*
|
||||
* @see content_translation_test_form_node_article_form_alter()
|
||||
*/
|
||||
public function formNodeFormSubmit($form, FormStateInterface $form_state): void {
|
||||
\Drupal::state()->set('test_field_only_en_fr', $form_state->getValue('test_field_only_en_fr'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
name: 'Content translation test views'
|
||||
type: module
|
||||
description: 'Provides default views for views content translation tests.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- drupal:content_translation
|
||||
- drupal:views
|
||||
@ -0,0 +1,112 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- content_translation
|
||||
- user
|
||||
id: test_entity_translations_link
|
||||
label: People
|
||||
module: views
|
||||
description: ''
|
||||
tag: ''
|
||||
base_table: users_field_data
|
||||
base_field: uid
|
||||
display:
|
||||
default:
|
||||
display_plugin: default
|
||||
id: default
|
||||
display_title: Default
|
||||
position: null
|
||||
display_options:
|
||||
access:
|
||||
type: none
|
||||
cache:
|
||||
type: tag
|
||||
query:
|
||||
type: views_query
|
||||
exposed_form:
|
||||
type: basic
|
||||
options:
|
||||
submit_button: Filter
|
||||
reset_button: true
|
||||
reset_button_label: Reset
|
||||
pager:
|
||||
type: full
|
||||
options:
|
||||
items_per_page: 50
|
||||
style:
|
||||
type: table
|
||||
options:
|
||||
class: ''
|
||||
columns:
|
||||
name: name
|
||||
translation_link: translation_link
|
||||
default: created
|
||||
row:
|
||||
type: fields
|
||||
fields:
|
||||
name:
|
||||
id: name
|
||||
table: users_field_data
|
||||
field: name
|
||||
label: Username
|
||||
plugin_id: field
|
||||
type: user_name
|
||||
entity_type: user
|
||||
entity_field: name
|
||||
translation_link:
|
||||
id: translation_link
|
||||
table: users
|
||||
field: translation_link
|
||||
label: 'Translation link'
|
||||
exclude: false
|
||||
alter:
|
||||
alter_text: false
|
||||
element_class: ''
|
||||
element_default_classes: true
|
||||
empty: ''
|
||||
hide_empty: false
|
||||
empty_zero: false
|
||||
hide_alter_empty: true
|
||||
text: Translate
|
||||
plugin_id: content_translation_link
|
||||
entity_type: user
|
||||
filters:
|
||||
uid_raw:
|
||||
id: uid_raw
|
||||
table: users_field_data
|
||||
field: uid_raw
|
||||
operator: '!='
|
||||
value:
|
||||
value: '0'
|
||||
group: 1
|
||||
exposed: false
|
||||
plugin_id: numeric
|
||||
entity_type: user
|
||||
sorts:
|
||||
created:
|
||||
id: created
|
||||
table: users_field_data
|
||||
field: created
|
||||
order: DESC
|
||||
plugin_id: date
|
||||
entity_type: user
|
||||
entity_field: created
|
||||
title: People
|
||||
empty:
|
||||
area:
|
||||
id: area
|
||||
table: views
|
||||
field: area
|
||||
empty: true
|
||||
content:
|
||||
value: 'No people available.'
|
||||
format: plain_text
|
||||
plugin_id: text
|
||||
page_1:
|
||||
display_plugin: page
|
||||
id: page_1
|
||||
display_title: Page
|
||||
position: null
|
||||
display_options:
|
||||
path: test-entity-translations-link
|
||||
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
/**
|
||||
* Tests the test content translation UI with the test entity.
|
||||
*
|
||||
* @group content_translation
|
||||
* @group #slow
|
||||
*/
|
||||
class ContentTestTranslationUITest extends ContentTranslationUITestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $testHTMLEscapeForAllLanguages = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultCacheContexts = [
|
||||
'languages:language_interface',
|
||||
'theme',
|
||||
'url.query_args:_wrapper_format',
|
||||
'user.permissions',
|
||||
'url.site',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
// Use the entity_test_mul as this has multilingual property support.
|
||||
$this->entityTypeId = 'entity_test_mul_changed';
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getTranslatorPermissions(): array {
|
||||
return array_merge(parent::getTranslatorPermissions(), ['administer entity_test content', 'view test entity']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
|
||||
|
||||
/**
|
||||
* Tests that contextual links are available for content translation.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationContextualLinksTest extends BrowserTestBase {
|
||||
|
||||
use ContentTranslationTestTrait;
|
||||
|
||||
/**
|
||||
* The bundle being tested.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The content type being tested.
|
||||
*
|
||||
* @var \Drupal\node\Entity\NodeType
|
||||
*/
|
||||
protected $contentType;
|
||||
|
||||
/**
|
||||
* The 'translator' user to use during testing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* The enabled languages.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $langcodes;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['content_translation', 'contextual', 'node'];
|
||||
|
||||
/**
|
||||
* The profile to install as a basis for testing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $profile = 'testing';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Set up an additional language.
|
||||
$this->langcodes = [\Drupal::languageManager()->getDefaultLanguage()->getId(), 'es'];
|
||||
static::createLanguageFromLangcode('es');
|
||||
|
||||
// Create a content type.
|
||||
$this->bundle = $this->randomMachineName();
|
||||
$this->contentType = $this->drupalCreateContentType(['type' => $this->bundle]);
|
||||
|
||||
// Add a field to the content type. The field is not yet translatable.
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => 'field_test_text',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'text',
|
||||
'cardinality' => 1,
|
||||
])->save();
|
||||
FieldConfig::create([
|
||||
'entity_type' => 'node',
|
||||
'field_name' => 'field_test_text',
|
||||
'bundle' => $this->bundle,
|
||||
'label' => 'Test text-field',
|
||||
])->save();
|
||||
$this->container->get('entity_display.repository')
|
||||
->getFormDisplay('node', $this->bundle)
|
||||
->setComponent('field_test_text', [
|
||||
'type' => 'text_textfield',
|
||||
'weight' => 0,
|
||||
])
|
||||
->save();
|
||||
|
||||
// Create a translator user.
|
||||
$permissions = [
|
||||
'access contextual links',
|
||||
'administer nodes',
|
||||
"edit any $this->bundle content",
|
||||
'translate any entity',
|
||||
];
|
||||
$this->translator = $this->drupalCreateUser($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a contextual link is available for translating a node.
|
||||
*/
|
||||
public function testContentTranslationContextualLinks(): void {
|
||||
// Create a node.
|
||||
$title = $this->randomString();
|
||||
$this->drupalCreateNode(['type' => $this->bundle, 'title' => $title, 'langcode' => 'en']);
|
||||
$node = $this->drupalGetNodeByTitle($title);
|
||||
|
||||
static::enableContentTranslation('node', $this->bundle);
|
||||
|
||||
// Check that the link leads to the translate page.
|
||||
$this->drupalLogin($this->translator);
|
||||
$translate_link = 'node/' . $node->id() . '/translations';
|
||||
$this->drupalGet($translate_link);
|
||||
$this->assertSession()->pageTextContains('Translations of ' . $node->label());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Test disabling content translation module.
|
||||
*
|
||||
* @covers \Drupal\language\Form\ContentLanguageSettingsForm
|
||||
* @covers ::_content_translation_form_language_content_settings_form_alter
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationDisableSettingTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'content_translation',
|
||||
'menu_link_content',
|
||||
'language',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Tests that entity schemas are up-to-date after enabling translation.
|
||||
*/
|
||||
public function testDisableSetting(): void {
|
||||
// Define selectors.
|
||||
$group_checkbox = 'entity_types[menu_link_content]';
|
||||
$translatable_checkbox = 'settings[menu_link_content][menu_link_content][translatable]';
|
||||
$language_alterable = 'settings[menu_link_content][menu_link_content][settings][language][language_alterable]';
|
||||
|
||||
$user = $this->drupalCreateUser([
|
||||
'administer site configuration',
|
||||
'administer content translation',
|
||||
'create content translations',
|
||||
'administer languages',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$assert = $this->assertSession();
|
||||
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
|
||||
$assert->checkboxNotChecked('entity_types[menu_link_content]');
|
||||
|
||||
$edit = [
|
||||
$group_checkbox => TRUE,
|
||||
$translatable_checkbox => TRUE,
|
||||
$language_alterable => TRUE,
|
||||
];
|
||||
$this->submitForm($edit, 'Save configuration');
|
||||
|
||||
$assert->statusMessageContains('Settings successfully updated.', 'status');
|
||||
|
||||
$assert->checkboxChecked($group_checkbox);
|
||||
|
||||
$edit = [
|
||||
$group_checkbox => FALSE,
|
||||
$translatable_checkbox => TRUE,
|
||||
$language_alterable => TRUE,
|
||||
];
|
||||
$this->submitForm($edit, 'Save configuration');
|
||||
|
||||
$assert->statusMessageContains('Settings successfully updated.', 'status');
|
||||
|
||||
$assert->checkboxNotChecked($group_checkbox);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
|
||||
/**
|
||||
* Test enabling content translation module.
|
||||
*
|
||||
* @covers \Drupal\language\Form\ContentLanguageSettingsForm
|
||||
* @covers ::_content_translation_form_language_content_settings_form_alter
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationEnableTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['entity_test', 'menu_link_content', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Tests that entity schemas are up-to-date after enabling translation.
|
||||
*/
|
||||
public function testEnable(): void {
|
||||
$this->rootUser = $this->drupalCreateUser([
|
||||
'administer modules',
|
||||
'administer site configuration',
|
||||
'administer content types',
|
||||
]);
|
||||
$this->drupalLogin($this->rootUser);
|
||||
// Enable modules and make sure the related config entity type definitions
|
||||
// are installed.
|
||||
$edit = [
|
||||
'modules[content_translation][enable]' => TRUE,
|
||||
'modules[language][enable]' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/modules');
|
||||
$this->submitForm($edit, 'Install');
|
||||
$this->rebuildContainer();
|
||||
|
||||
// Status messages are shown.
|
||||
$this->assertSession()->statusMessageContains('This site has only a single language enabled. Add at least one more language in order to translate content.', 'warning');
|
||||
$this->assertSession()->statusMessageContains('Enable translation for content types, taxonomy vocabularies, accounts, or any other element you wish to translate.', 'warning');
|
||||
|
||||
// No pending updates should be available.
|
||||
$this->drupalGet('admin/reports/status');
|
||||
$this->assertSession()->elementTextEquals('css', "details.system-status-report__entry summary:contains('Entity/field definitions') + div", 'Up to date');
|
||||
|
||||
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), [
|
||||
'administer content translation',
|
||||
'administer languages',
|
||||
]);
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
// The node entity type should not be an option because it has no bundles.
|
||||
$this->assertSession()->responseNotContains('entity_types[node]');
|
||||
// Enable content translation on entity types that have will have a
|
||||
// content_translation_uid.
|
||||
$edit = [
|
||||
'entity_types[menu_link_content]' => TRUE,
|
||||
'settings[menu_link_content][menu_link_content][translatable]' => TRUE,
|
||||
'entity_types[entity_test_mul]' => TRUE,
|
||||
'settings[entity_test_mul][entity_test_mul][translatable]' => TRUE,
|
||||
];
|
||||
$this->submitForm($edit, 'Save configuration');
|
||||
|
||||
// No pending updates should be available.
|
||||
$this->drupalGet('admin/reports/status');
|
||||
$this->assertSession()->elementTextEquals('css', "details.system-status-report__entry summary:contains('Entity/field definitions') + div", 'Up to date');
|
||||
|
||||
// Create a node type and check the content translation settings are now
|
||||
// available for nodes.
|
||||
$edit = [
|
||||
'name' => 'foo',
|
||||
'title_label' => 'title for foo',
|
||||
'type' => 'foo',
|
||||
];
|
||||
$this->drupalGet('admin/structure/types/add');
|
||||
$this->submitForm($edit, 'Save');
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
$this->assertSession()->responseContains('entity_types[node]');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the content translation behaviors on entity bundle UI.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationEntityBundleUITest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'node',
|
||||
'comment',
|
||||
'field_ui',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$user = $this->drupalCreateUser([
|
||||
'access administration pages',
|
||||
'administer languages',
|
||||
'administer content translation',
|
||||
'administer content types',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests content types default translation behavior.
|
||||
*/
|
||||
public function testContentTypeUI(): void {
|
||||
// Create first content type.
|
||||
$this->drupalCreateContentType(['type' => 'article']);
|
||||
// Enable content translation.
|
||||
$edit = ['language_configuration[content_translation]' => TRUE];
|
||||
$this->drupalGet('admin/structure/types/manage/article');
|
||||
$this->submitForm($edit, 'Save');
|
||||
|
||||
// Make sure add page does not inherit translation configuration from first
|
||||
// content type.
|
||||
$this->drupalGet('admin/structure/types/add');
|
||||
$this->assertSession()->checkboxNotChecked('edit-language-configuration-content-translation');
|
||||
|
||||
// Create second content type and set content translation.
|
||||
$edit = [
|
||||
'name' => 'Page',
|
||||
'type' => 'page',
|
||||
'language_configuration[content_translation]' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/structure/types/add');
|
||||
$this->submitForm($edit, 'Save and manage fields');
|
||||
|
||||
// Make sure the settings are saved when creating the content type.
|
||||
$this->drupalGet('admin/structure/types/manage/page');
|
||||
$this->assertSession()->checkboxChecked('edit-language-configuration-content-translation');
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
|
||||
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
|
||||
use Drupal\Tests\node\Functional\NodeTestBase;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* Tests the content translation language that is set.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationLanguageChangeTest extends NodeTestBase {
|
||||
|
||||
use ContentTranslationTestTrait;
|
||||
use ImageFieldCreationTrait;
|
||||
use TestFileCreationTrait {
|
||||
getTestFiles as drupalGetTestFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'content_translation_test',
|
||||
'node',
|
||||
'block',
|
||||
'field_ui',
|
||||
'image',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$langcodes = ['de', 'fr'];
|
||||
foreach ($langcodes as $langcode) {
|
||||
static::createLanguageFromLangcode($langcode);
|
||||
}
|
||||
$this->drupalPlaceBlock('local_tasks_block');
|
||||
$user = $this->drupalCreateUser([
|
||||
'administer site configuration',
|
||||
'administer nodes',
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'delete any article content',
|
||||
'administer content translation',
|
||||
'translate any entity',
|
||||
'create content translations',
|
||||
'administer languages',
|
||||
'administer content types',
|
||||
'administer node fields',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
// Enable translations for article.
|
||||
$this->enableContentTranslation('node', 'article');
|
||||
|
||||
$this->rebuildContainer();
|
||||
|
||||
$this->createImageField('field_image_field', 'node', 'article');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the source language is properly set when changing.
|
||||
*/
|
||||
public function testLanguageChange(): void {
|
||||
// Create a node in English.
|
||||
$this->drupalGet('node/add/article');
|
||||
$edit = [
|
||||
'title[0][value]' => 'english_title',
|
||||
];
|
||||
$this->submitForm($edit, 'Save');
|
||||
|
||||
// Create a translation in French.
|
||||
$this->clickLink('Translate');
|
||||
$this->clickLink('Add');
|
||||
$this->submitForm([], 'Save (this translation)');
|
||||
$this->clickLink('Translate');
|
||||
|
||||
// Edit English translation.
|
||||
$this->clickLink('Edit', 1);
|
||||
// Upload and image after changing the node language.
|
||||
$images = $this->drupalGetTestFiles('image')[1];
|
||||
$edit = [
|
||||
'langcode[0][value]' => 'de',
|
||||
'files[field_image_field_0]' => $images->uri,
|
||||
];
|
||||
$this->submitForm($edit, 'Upload');
|
||||
$this->submitForm(['field_image_field[0][alt]' => 'alternative_text'], 'Save (this translation)');
|
||||
|
||||
// Check that the translation languages are correct.
|
||||
$node = $this->getNodeByTitle('english_title');
|
||||
$translation_languages = $node->getTranslationLanguages();
|
||||
$this->assertArrayHasKey('fr', $translation_languages);
|
||||
$this->assertArrayHasKey('de', $translation_languages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that title does not change on ajax call with new language value.
|
||||
*/
|
||||
public function testTitleDoesNotChangesOnChangingLanguageWidgetAndTriggeringAjaxCall(): void {
|
||||
// Create a node in English.
|
||||
$this->drupalGet('node/add/article', ['query' => ['test_field_only_en_fr' => 1]]);
|
||||
$edit = [
|
||||
'title[0][value]' => 'english_title',
|
||||
'test_field_only_en_fr' => 'node created',
|
||||
];
|
||||
$this->submitForm($edit, 'Save');
|
||||
$this->assertEquals('node created', \Drupal::state()->get('test_field_only_en_fr'));
|
||||
|
||||
// Create a translation in French.
|
||||
$this->clickLink('Translate');
|
||||
$this->clickLink('Add');
|
||||
$this->submitForm([], 'Save (this translation)');
|
||||
$this->clickLink('Translate');
|
||||
|
||||
// Edit English translation.
|
||||
$node = $this->getNodeByTitle('english_title');
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
// Test the expected title when loading the form.
|
||||
$this->assertSession()->titleEquals('Edit Article english_title | Drupal');
|
||||
// Upload and image after changing the node language.
|
||||
$images = $this->drupalGetTestFiles('image')[1];
|
||||
$edit = [
|
||||
'langcode[0][value]' => 'de',
|
||||
'files[field_image_field_0]' => $images->uri,
|
||||
];
|
||||
$this->submitForm($edit, 'Upload');
|
||||
// Test the expected title after triggering an ajax call with a new
|
||||
// language selected.
|
||||
$this->assertSession()->titleEquals('Edit Article english_title | Drupal');
|
||||
$edit = [
|
||||
'langcode[0][value]' => 'en',
|
||||
'field_image_field[0][alt]' => 'alternative_text',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
|
||||
// Check that the translation languages are correct.
|
||||
$node = $this->getNodeByTitle('english_title');
|
||||
$translation_languages = $node->getTranslationLanguages();
|
||||
$this->assertArrayHasKey('fr', $translation_languages);
|
||||
$this->assertArrayNotHasKey('de', $translation_languages);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\entity_test\Entity\EntityTestMul;
|
||||
use Drupal\content_translation_test\Entity\EntityTestTranslatableNoUISkip;
|
||||
|
||||
/**
|
||||
* Tests whether canonical link tags are present for content entities.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationLinkTagTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'entity_test',
|
||||
'content_translation',
|
||||
'content_translation_test',
|
||||
'language',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The added languages.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $langcodes;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Set up user.
|
||||
$user = $this->drupalCreateUser([
|
||||
'view test entity',
|
||||
'view test entity translations',
|
||||
'administer entity_test content',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
// Add additional languages.
|
||||
$this->langcodes = ['it', 'fr'];
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
ConfigurableLanguage::createFromLangcode($langcode)->save();
|
||||
}
|
||||
|
||||
// Rebuild the container so that the new languages are picked up by services
|
||||
// that hold a list of languages.
|
||||
$this->rebuildContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test entity with translations.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* An entity with translations.
|
||||
*/
|
||||
protected function createTranslatableEntity(): EntityInterface {
|
||||
$entity = EntityTestMul::create(['label' => $this->randomString()]);
|
||||
|
||||
// Create translations for non default languages.
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
$entity->addTranslation($langcode, ['label' => $this->randomString()]);
|
||||
}
|
||||
$entity->save();
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests alternate link tag found for entity types with canonical links.
|
||||
*/
|
||||
public function testCanonicalAlternateTags(): void {
|
||||
/** @var \Drupal\Core\Language\LanguageManagerInterface $languageManager */
|
||||
$languageManager = $this->container->get('language_manager');
|
||||
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager */
|
||||
$entityTypeManager = $this->container->get('entity_type.manager');
|
||||
|
||||
$definition = $entityTypeManager->getDefinition('entity_test_mul');
|
||||
$this->assertTrue($definition->hasLinkTemplate('canonical'), 'Canonical link template found for entity_test.');
|
||||
|
||||
$entity = $this->createTranslatableEntity();
|
||||
$url_base = $entity->toUrl('canonical')
|
||||
->setAbsolute();
|
||||
|
||||
$langcodes_all = $this->langcodes;
|
||||
$langcodes_all[] = $languageManager
|
||||
->getDefaultLanguage()
|
||||
->getId();
|
||||
|
||||
/** @var \Drupal\Core\Url[] $urls */
|
||||
$urls = array_map(
|
||||
function ($langcode) use ($url_base, $languageManager) {
|
||||
$url = clone $url_base;
|
||||
return $url
|
||||
->setOption('language', $languageManager->getLanguage($langcode));
|
||||
},
|
||||
array_combine($langcodes_all, $langcodes_all)
|
||||
);
|
||||
|
||||
// Ensure link tags for all languages are found on each language variation
|
||||
// page of an entity.
|
||||
foreach ($urls as $url) {
|
||||
$this->drupalGet($url);
|
||||
foreach ($urls as $langcode_alternate => $url_alternate) {
|
||||
$this->assertSession()->elementAttributeContains('xpath', "head/link[@rel='alternate' and @hreflang='$langcode_alternate']", 'href', $url_alternate->toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Configure entity path as a front page.
|
||||
$entity_canonical = '/entity_test_mul/manage/' . $entity->id();
|
||||
$this->config('system.site')->set('page.front', $entity_canonical)->save();
|
||||
|
||||
// Tests hreflang when using entities as a front page.
|
||||
foreach ($urls as $url) {
|
||||
$this->drupalGet($url);
|
||||
foreach ($entity->getTranslationLanguages() as $language) {
|
||||
$frontpage_path = Url::fromRoute('<front>', [], [
|
||||
'absolute' => TRUE,
|
||||
'language' => $language,
|
||||
])->toString();
|
||||
$this->assertSession()->elementAttributeContains('xpath', "head/link[@rel='alternate' and @hreflang='{$language->getId()}']", 'href', $frontpage_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests alternate link tag missing for entity types without canonical links.
|
||||
*/
|
||||
public function testCanonicalAlternateTagsMissing(): void {
|
||||
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager */
|
||||
$entityTypeManager = $this->container->get('entity_type.manager');
|
||||
|
||||
$definition = $entityTypeManager->getDefinition('entity_test_translatable_no_skip');
|
||||
// Ensure 'canonical' link template does not exist, in case it is added in
|
||||
// the future.
|
||||
$this->assertFalse($definition->hasLinkTemplate('canonical'), 'Canonical link template does not exist for entity_test_translatable_no_skip entity.');
|
||||
|
||||
$entity = EntityTestTranslatableNoUISkip::create();
|
||||
$entity->save();
|
||||
$this->drupalGet($entity->toUrl('edit-form'));
|
||||
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->elementNotExists('xpath', '//link[@rel="alternate" and @hreflang]');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
/**
|
||||
* Tests the Content Translation metadata fields handling.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationMetadataFieldsTest extends ContentTranslationTestBase {
|
||||
|
||||
/**
|
||||
* The entity type being tested.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId = 'node';
|
||||
|
||||
/**
|
||||
* The bundle being tested.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle = 'article';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['language', 'content_translation', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setupBundle(): void {
|
||||
parent::setupBundle();
|
||||
$this->createContentType(['type' => $this->bundle]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests skipping setting non translatable metadata fields.
|
||||
*/
|
||||
public function testSkipUntranslatable(): void {
|
||||
$this->drupalLogin($this->translator);
|
||||
$fields = \Drupal::service('entity_field.manager')->getFieldDefinitions($this->entityTypeId, $this->bundle);
|
||||
|
||||
// Turn off translatability for the metadata fields on the current bundle.
|
||||
$metadata_fields = ['created', 'changed', 'uid', 'status'];
|
||||
foreach ($metadata_fields as $field_name) {
|
||||
$fields[$field_name]
|
||||
->getConfig($this->bundle)
|
||||
->setTranslatable(FALSE)
|
||||
->save();
|
||||
}
|
||||
|
||||
// Create a new test entity with original values in the default language.
|
||||
$default_langcode = $this->langcodes[0];
|
||||
$entity_id = $this->createEntity(['title' => $this->randomString()], $default_langcode);
|
||||
$storage = \Drupal::entityTypeManager()->getStorage($this->entityTypeId);
|
||||
$storage->resetCache();
|
||||
$entity = $storage->load($entity_id);
|
||||
|
||||
// Add a content translation.
|
||||
$langcode = 'it';
|
||||
$values = $entity->toArray();
|
||||
// Apply a default value for the metadata fields.
|
||||
foreach ($metadata_fields as $field_name) {
|
||||
unset($values[$field_name]);
|
||||
}
|
||||
$entity->addTranslation($langcode, $values);
|
||||
|
||||
$metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode));
|
||||
$metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
|
||||
|
||||
$created_time = $metadata_source_translation->getCreatedTime();
|
||||
$changed_time = $metadata_source_translation->getChangedTime();
|
||||
$published = $metadata_source_translation->isPublished();
|
||||
$author = $metadata_source_translation->getAuthor();
|
||||
|
||||
$this->assertEquals($created_time, $metadata_target_translation->getCreatedTime(), 'Metadata created field has the same value for both translations.');
|
||||
$this->assertEquals($changed_time, $metadata_target_translation->getChangedTime(), 'Metadata changed field has the same value for both translations.');
|
||||
$this->assertEquals($published, $metadata_target_translation->isPublished(), 'Metadata published field has the same value for both translations.');
|
||||
$this->assertEquals($author->id(), $metadata_target_translation->getAuthor()->id(), 'Metadata author field has the same value for both translations.');
|
||||
|
||||
$metadata_target_translation->setCreatedTime(time() + 50);
|
||||
$metadata_target_translation->setChangedTime(time() + 50);
|
||||
$metadata_target_translation->setPublished(TRUE);
|
||||
$metadata_target_translation->setAuthor($this->editor);
|
||||
|
||||
$this->assertEquals($created_time, $metadata_target_translation->getCreatedTime(), 'Metadata created field correctly not updated');
|
||||
$this->assertEquals($changed_time, $metadata_target_translation->getChangedTime(), 'Metadata changed field correctly not updated');
|
||||
$this->assertEquals($published, $metadata_target_translation->isPublished(), 'Metadata published field correctly not updated');
|
||||
$this->assertEquals($author->id(), $metadata_target_translation->getAuthor()->id(), 'Metadata author field correctly not updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests setting translatable metadata fields.
|
||||
*/
|
||||
public function testSetTranslatable(): void {
|
||||
$this->drupalLogin($this->translator);
|
||||
$fields = \Drupal::service('entity_field.manager')->getFieldDefinitions($this->entityTypeId, $this->bundle);
|
||||
|
||||
// Turn off translatability for the metadata fields on the current bundle.
|
||||
$metadata_fields = ['created', 'changed', 'uid', 'status'];
|
||||
foreach ($metadata_fields as $field_name) {
|
||||
$fields[$field_name]
|
||||
->getConfig($this->bundle)
|
||||
->setTranslatable(TRUE)
|
||||
->save();
|
||||
}
|
||||
|
||||
// Create a new test entity with original values in the default language.
|
||||
$default_langcode = $this->langcodes[0];
|
||||
$entity_id = $this->createEntity(['title' => $this->randomString(), 'status' => FALSE], $default_langcode);
|
||||
$storage = \Drupal::entityTypeManager()->getStorage($this->entityTypeId);
|
||||
$storage->resetCache();
|
||||
$entity = $storage->load($entity_id);
|
||||
|
||||
// Add a content translation.
|
||||
$langcode = 'it';
|
||||
$values = $entity->toArray();
|
||||
// Apply a default value for the metadata fields.
|
||||
foreach ($metadata_fields as $field_name) {
|
||||
unset($values[$field_name]);
|
||||
}
|
||||
$entity->addTranslation($langcode, $values);
|
||||
|
||||
$metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode));
|
||||
$metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
|
||||
|
||||
$metadata_target_translation->setCreatedTime(time() + 50);
|
||||
$metadata_target_translation->setChangedTime(time() + 50);
|
||||
$metadata_target_translation->setPublished(TRUE);
|
||||
$metadata_target_translation->setAuthor($this->editor);
|
||||
|
||||
$this->assertNotEquals($metadata_source_translation->getCreatedTime(), $metadata_target_translation->getCreatedTime(), 'Metadata created field correctly different on both translations.');
|
||||
$this->assertNotEquals($metadata_source_translation->getChangedTime(), $metadata_target_translation->getChangedTime(), 'Metadata changed field correctly different on both translations.');
|
||||
$this->assertNotEquals($metadata_source_translation->isPublished(), $metadata_target_translation->isPublished(), 'Metadata published field correctly different on both translations.');
|
||||
$this->assertNotEquals($metadata_source_translation->getAuthor()->id(), $metadata_target_translation->getAuthor()->id(), 'Metadata author field correctly different on both translations.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests that new translations do not delete existing ones.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationNewTranslationWithExistingRevisionsTest extends ContentTranslationPendingRevisionTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'content_moderation',
|
||||
'content_translation',
|
||||
'content_translation_test',
|
||||
'language',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
$this->enableContentModeration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a translation with a draft is not deleted.
|
||||
*/
|
||||
public function testDraftTranslationIsNotDeleted(): void {
|
||||
$this->drupalLogin($this->translator);
|
||||
|
||||
// Create a test node.
|
||||
$values = [
|
||||
'title' => "Test EN",
|
||||
'moderation_state' => 'published',
|
||||
];
|
||||
$id = $this->createEntity($values, 'en');
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->storage->load($id);
|
||||
|
||||
// Add a published translation.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
|
||||
[
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'it',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('it'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalGet($add_translation_url);
|
||||
$edit = [
|
||||
'title[0][value]' => "Test IT",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
|
||||
// Add a draft translation.
|
||||
$this->drupalGet($this->getEditUrl($it_revision));
|
||||
$edit = [
|
||||
'title[0][value]' => "Test IT 2",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
|
||||
// Add a new draft translation.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
|
||||
[
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'fr',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('fr'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalGet($add_translation_url);
|
||||
$edit = [
|
||||
'title[0][value]' => "Test FR",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
// Check the first translation still exists.
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test translation delete hooks are not invoked.
|
||||
*/
|
||||
public function testCreatingNewDraftDoesNotInvokeDeleteHook(): void {
|
||||
$this->drupalLogin($this->translator);
|
||||
|
||||
// Create a test node.
|
||||
$values = [
|
||||
'title' => "Test EN",
|
||||
'moderation_state' => 'published',
|
||||
];
|
||||
$id = $this->createEntity($values, 'en');
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->storage->load($id);
|
||||
|
||||
// Add a published translation.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
|
||||
[
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'it',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('it'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalGet($add_translation_url);
|
||||
$edit = [
|
||||
'title[0][value]' => "Test IT",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
|
||||
// Add a draft translation.
|
||||
$this->drupalGet($this->getEditUrl($it_revision));
|
||||
$edit = [
|
||||
'title[0][value]' => "Test IT 2",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
// Add a new draft translation.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
|
||||
[
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'fr',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('fr'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalGet($add_translation_url);
|
||||
$edit = [
|
||||
'title[0][value]' => "Test FR",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
// If the translation delete hook was incorrectly invoked, the state
|
||||
// variable would be set.
|
||||
$this->assertNull($this->container->get('state')->get('content_translation_test.translation_deleted'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Tests\language\Traits\LanguageTestTrait;
|
||||
use Drupal\Tests\node\Functional\NodeTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
|
||||
/**
|
||||
* Tests the content translation operations available in the content listing.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationOperationsTest extends NodeTestBase {
|
||||
|
||||
use LanguageTestTrait;
|
||||
|
||||
/**
|
||||
* A base user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User|false
|
||||
*/
|
||||
protected $baseUser1;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* A base user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User|false
|
||||
*/
|
||||
protected $baseUser2;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'node',
|
||||
'views',
|
||||
'block',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Enable additional languages.
|
||||
$langcodes = ['es', 'ast'];
|
||||
foreach ($langcodes as $langcode) {
|
||||
static::createLanguageFromLangcode($langcode);
|
||||
}
|
||||
|
||||
// Enable translation for the current entity type and ensure the change is
|
||||
// picked up.
|
||||
\Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
|
||||
|
||||
$this->baseUser1 = $this->drupalCreateUser(['access content overview']);
|
||||
$this->baseUser2 = $this->drupalCreateUser([
|
||||
'access content overview',
|
||||
'create content translations',
|
||||
'update content translations',
|
||||
'delete content translations',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the operation "Translate" is displayed in the content listing.
|
||||
*/
|
||||
public function testOperationTranslateLink(): void {
|
||||
$node = $this->drupalCreateNode(['type' => 'article', 'langcode' => 'es']);
|
||||
// Verify no translation operation links are displayed for users without
|
||||
// permission.
|
||||
$this->drupalLogin($this->baseUser1);
|
||||
$this->drupalGet('admin/content');
|
||||
$this->assertSession()->linkByHrefNotExists('node/' . $node->id() . '/translations');
|
||||
$this->drupalLogout();
|
||||
// Verify there's a translation operation link for users with enough
|
||||
// permissions.
|
||||
$this->drupalLogin($this->baseUser2);
|
||||
$this->drupalGet('admin/content');
|
||||
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/translations');
|
||||
|
||||
// Ensure that an unintended misconfiguration of permissions does not open
|
||||
// access to the translation form, see https://www.drupal.org/node/2558905.
|
||||
$this->drupalLogout();
|
||||
user_role_change_permissions(
|
||||
Role::AUTHENTICATED_ID,
|
||||
[
|
||||
'create content translations' => TRUE,
|
||||
'access content' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalLogin($this->baseUser1);
|
||||
$this->drupalGet($node->toUrl('drupal:content-translation-overview'));
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
|
||||
// Ensure that the translation overview is also not accessible when the user
|
||||
// has 'access content', but the node is not published.
|
||||
user_role_change_permissions(
|
||||
Role::AUTHENTICATED_ID,
|
||||
[
|
||||
'create content translations' => TRUE,
|
||||
'access content' => TRUE,
|
||||
]
|
||||
);
|
||||
$node->setUnpublished()->save();
|
||||
$this->drupalGet($node->toUrl('drupal:content-translation-overview'));
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
$this->drupalLogout();
|
||||
|
||||
// Ensure the 'Translate' local task does not show up anymore when disabling
|
||||
// translations for a content type.
|
||||
$node->setPublished()->save();
|
||||
user_role_change_permissions(
|
||||
Role::AUTHENTICATED_ID,
|
||||
[
|
||||
'administer content translation' => TRUE,
|
||||
'administer languages' => TRUE,
|
||||
]
|
||||
);
|
||||
$this->drupalPlaceBlock('local_tasks_block');
|
||||
$this->drupalLogin($this->baseUser2);
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/translations');
|
||||
static::disableBundleTranslation('node', 'article');
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
$this->assertSession()->linkByHrefNotExists('node/' . $node->id() . '/translations');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the access to the overview page for translations.
|
||||
*
|
||||
* @see content_translation_translate_access()
|
||||
*/
|
||||
public function testContentTranslationOverviewAccess(): void {
|
||||
$access_control_handler = \Drupal::entityTypeManager()->getAccessControlHandler('node');
|
||||
$user = $this->createUser(['create content translations', 'access content']);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$node = $this->drupalCreateNode(['status' => FALSE, 'type' => 'article']);
|
||||
$this->assertFalse(content_translation_translate_access($node)->isAllowed());
|
||||
$access_control_handler->resetCache();
|
||||
|
||||
$node->setPublished();
|
||||
$node->save();
|
||||
$this->assertTrue(content_translation_translate_access($node)->isAllowed());
|
||||
$access_control_handler->resetCache();
|
||||
|
||||
user_role_change_permissions(
|
||||
Role::AUTHENTICATED_ID,
|
||||
[
|
||||
'access content' => FALSE,
|
||||
]
|
||||
);
|
||||
|
||||
$user = $this->createUser(['create content translations']);
|
||||
$this->drupalLogin($user);
|
||||
$this->assertFalse(content_translation_translate_access($node)->isAllowed());
|
||||
$access_control_handler->resetCache();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests the "Flag as outdated" functionality with revision translations.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationOutdatedRevisionTranslationTest extends ContentTranslationPendingRevisionTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
$this->enableContentModeration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that outdated revision translations work correctly.
|
||||
*/
|
||||
public function testFlagAsOutdatedHidden(): void {
|
||||
// Create a test node.
|
||||
$values = [
|
||||
'title' => 'Test 1.1 EN',
|
||||
'moderation_state' => 'published',
|
||||
];
|
||||
$id = $this->createEntity($values, 'en');
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->storage->load($id);
|
||||
|
||||
// Add a published Italian translation.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
|
||||
[
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'it',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('it'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalGet($add_translation_url);
|
||||
$this->assertFlagWidget();
|
||||
$edit = [
|
||||
'title[0][value]' => 'Test 1.2 IT',
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
|
||||
// Add a published French translation.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
|
||||
[
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'fr',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('fr'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalGet($add_translation_url);
|
||||
$this->assertFlagWidget();
|
||||
$edit = [
|
||||
'title[0][value]' => 'Test 1.3 FR',
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
|
||||
// Create an English draft.
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$en_edit_url = $this->getEditUrl($entity);
|
||||
$this->drupalGet($en_edit_url);
|
||||
$this->assertFlagWidget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the flag widget is displayed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertFlagWidget(): void {
|
||||
$this->assertSession()->pageTextNotContains('Flag other translations as outdated');
|
||||
$this->assertSession()->pageTextContains('Translations cannot be flagged as outdated when content is moderated.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* Base class for pending revision translation tests.
|
||||
*/
|
||||
abstract class ContentTranslationPendingRevisionTestBase extends ContentTranslationTestBase {
|
||||
|
||||
use ContentTypeCreationTrait;
|
||||
use ContentModerationTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'content_moderation',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
* The entity storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Permissions common to all test accounts.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $commonPermissions;
|
||||
|
||||
/**
|
||||
* The current test account.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentAccount;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
$this->entityTypeId = 'node';
|
||||
$this->bundle = 'article';
|
||||
|
||||
$this->commonPermissions = [
|
||||
'view any unpublished content',
|
||||
"translate {$this->bundle} {$this->entityTypeId}",
|
||||
"create content translations",
|
||||
'use editorial transition create_new_draft',
|
||||
'use editorial transition publish',
|
||||
'use editorial transition archive',
|
||||
'use editorial transition archived_draft',
|
||||
'use editorial transition archived_published',
|
||||
];
|
||||
|
||||
parent::setUp();
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
|
||||
$entity_type_manager = $this->container->get('entity_type.manager');
|
||||
$this->storage = $entity_type_manager->getStorage($this->entityTypeId);
|
||||
|
||||
// @todo Remove this line once https://www.drupal.org/node/2945928 is fixed.
|
||||
$this->config('node.settings')->set('use_admin_theme', '1')->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables content moderation for the test entity type and bundle.
|
||||
*/
|
||||
protected function enableContentModeration() {
|
||||
$perms = array_merge(parent::getAdministratorPermissions(), [
|
||||
'administer workflows',
|
||||
'view latest version',
|
||||
]);
|
||||
$this->rootUser = $this->drupalCreateUser($perms);
|
||||
$this->drupalLogin($this->rootUser);
|
||||
$workflow_id = 'editorial';
|
||||
$this->drupalGet('/admin/config/workflow/workflows');
|
||||
$edit['bundles[' . $this->bundle . ']'] = TRUE;
|
||||
$this->drupalGet('admin/config/workflow/workflows/manage/' . $workflow_id . '/type/' . $this->entityTypeId);
|
||||
$this->submitForm($edit, 'Save');
|
||||
// Ensure the parent environment is up-to-date.
|
||||
// @see content_moderation_workflow_insert()
|
||||
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
/** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
|
||||
$router_builder = $this->container->get('router.builder');
|
||||
$router_builder->rebuildIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditorPermissions() {
|
||||
$editor_permissions = [
|
||||
"edit any {$this->bundle} content",
|
||||
"delete any {$this->bundle} content",
|
||||
"view {$this->bundle} revisions",
|
||||
"delete {$this->bundle} revisions",
|
||||
];
|
||||
return array_merge($editor_permissions, $this->commonPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getTranslatorPermissions() {
|
||||
return array_merge(parent::getTranslatorPermissions(), $this->commonPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setupBundle() {
|
||||
parent::setupBundle();
|
||||
$this->createContentType(['type' => $this->bundle]);
|
||||
$this->createEditorialWorkflow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the active revision translation for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being edited.
|
||||
* @param string $langcode
|
||||
* The translation language code.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface|null
|
||||
* The active revision translation or NULL if none could be identified.
|
||||
*/
|
||||
protected function loadRevisionTranslation(ContentEntityInterface $entity, $langcode) {
|
||||
// Explicitly invalidate the cache for that node, as the call below is
|
||||
// statically cached.
|
||||
$this->storage->resetCache([$entity->id()]);
|
||||
$revision_id = $this->storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||
$revision = $revision_id ? $this->storage->loadRevision($revision_id) : NULL;
|
||||
return $revision && $revision->hasTranslation($langcode) ? $revision->getTranslation($langcode) : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the edit URL for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being edited.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The edit URL.
|
||||
*/
|
||||
protected function getEditUrl(ContentEntityInterface $entity) {
|
||||
if ($entity->access('update', $this->loggedInUser)) {
|
||||
$url = $entity->toUrl('edit-form');
|
||||
}
|
||||
else {
|
||||
$url = $entity->toUrl('drupal:content-translation-edit');
|
||||
$url->setRouteParameter('language', $entity->language()->getId());
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the delete translation URL for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being edited.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The delete translation URL.
|
||||
*/
|
||||
protected function getDeleteUrl(ContentEntityInterface $entity) {
|
||||
if ($entity->access('delete', $this->loggedInUser)) {
|
||||
$url = $entity->toUrl('delete-form');
|
||||
}
|
||||
else {
|
||||
$url = $entity->toUrl('drupal:content-translation-delete');
|
||||
$url->setRouteParameter('language', $entity->language()->getId());
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests that revision translation deletion is handled correctly.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationRevisionTranslationDeletionTest extends ContentTranslationPendingRevisionTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
$this->enableContentModeration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that translation overview handles pending revisions correctly.
|
||||
*/
|
||||
public function testOverview(): void {
|
||||
$index = 1;
|
||||
$accounts = [
|
||||
$this->rootUser,
|
||||
$this->editor,
|
||||
$this->translator,
|
||||
];
|
||||
foreach ($accounts as $account) {
|
||||
$this->currentAccount = $account;
|
||||
$this->doTestOverview($index++);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a test run.
|
||||
*
|
||||
* @param int $index
|
||||
* The test run index.
|
||||
*/
|
||||
public function doTestOverview($index): void {
|
||||
$this->drupalLogin($this->currentAccount);
|
||||
|
||||
// Create a test node.
|
||||
$values = [
|
||||
'title' => "Test $index.1 EN",
|
||||
'moderation_state' => 'published',
|
||||
];
|
||||
$id = $this->createEntity($values, 'en');
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->storage->load($id);
|
||||
|
||||
// Add a draft translation and check that it is available only in the latest
|
||||
// revision.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add",
|
||||
[
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'it',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('it'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$add_translation_href = $add_translation_url->toString();
|
||||
$this->drupalGet($add_translation_url);
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.2 IT",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertFalse($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
|
||||
// Check that translations cannot be deleted in drafts.
|
||||
$overview_url = $entity->toUrl('drupal:content-translation-overview');
|
||||
$this->drupalGet($overview_url);
|
||||
$it_delete_url = $this->getDeleteUrl($it_revision);
|
||||
$it_delete_href = $it_delete_url->toString();
|
||||
$this->assertSession()->linkByHrefNotExists($it_delete_href);
|
||||
$warning = 'The "Delete translation" action is only available for published translations.';
|
||||
$this->assertSession()->statusMessageContains($warning, 'warning');
|
||||
$this->drupalGet($this->getEditUrl($it_revision));
|
||||
$this->assertSession()->linkNotExistsExact('Delete translation');
|
||||
|
||||
// Publish the translation and verify it can be deleted.
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.3 IT",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefExists($it_delete_href);
|
||||
$this->assertSession()->statusMessageNotContains($warning);
|
||||
$this->drupalGet($this->getEditUrl($it_revision));
|
||||
$this->assertSession()->linkExistsExact('Delete translation');
|
||||
|
||||
// Create an English draft and verify the published translation was
|
||||
// preserved.
|
||||
$this->drupalLogin($this->editor);
|
||||
$en_revision = $this->loadRevisionTranslation($entity, 'en');
|
||||
$this->drupalGet($this->getEditUrl($en_revision));
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.4 EN",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
$en_revision = $this->loadRevisionTranslation($entity, 'en');
|
||||
$this->assertTrue($en_revision->hasTranslation('it'));
|
||||
$this->drupalLogin($this->currentAccount);
|
||||
|
||||
// Delete the translation and verify that it is actually gone and that it is
|
||||
// possible to create it again.
|
||||
$this->drupalGet($it_delete_url);
|
||||
$this->submitForm([], 'Delete Italian translation');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertFalse($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->wasDefaultRevision());
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->assertLessThan($entity->getRevisionId(), $it_revision->getRevisionId());
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString());
|
||||
$this->assertSession()->linkByHrefExists($add_translation_href);
|
||||
|
||||
// Publish the English draft and verify the translation is not accidentally
|
||||
// restored.
|
||||
$this->drupalLogin($this->editor);
|
||||
$en_revision = $this->loadRevisionTranslation($entity, 'en');
|
||||
$this->drupalGet($this->getEditUrl($en_revision));
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.6 EN",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->submitForm($edit, 'Save');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertFalse($entity->hasTranslation('it'));
|
||||
$this->drupalLogin($this->currentAccount);
|
||||
|
||||
// Create a published translation again and verify it could be deleted.
|
||||
$this->drupalGet($add_translation_url);
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.7 IT",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefExists($it_delete_href);
|
||||
|
||||
// Create a translation draft again and verify it cannot be deleted.
|
||||
$this->drupalGet($this->getEditUrl($it_revision));
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.8 IT",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->submitForm($edit, 'Save (this translation)');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefNotExists($it_delete_href);
|
||||
|
||||
// Delete the translation draft and verify the translation can be deleted
|
||||
// again, since the active revision is now a default revision.
|
||||
$this->drupalLogin($this->editor);
|
||||
$this->drupalGet($it_revision->toUrl('version-history'));
|
||||
$revision_deletion_url = Url::fromRoute('node.revision_delete_confirm',
|
||||
[
|
||||
'node' => $id,
|
||||
'node_revision' => $it_revision->getRevisionId(),
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('it'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$revision_deletion_href = $revision_deletion_url->toString();
|
||||
$this->getSession()->getDriver()->click("//a[@href='$revision_deletion_href']");
|
||||
$this->submitForm([], 'Delete');
|
||||
$this->drupalLogin($this->currentAccount);
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefExists($it_delete_href);
|
||||
|
||||
// Verify that now the translation can be deleted.
|
||||
$this->drupalGet($this->getEditUrl($it_revision)->setOption('query', ['destination', '/kittens']));
|
||||
$this->clickLink('Delete translation');
|
||||
$this->submitForm([], 'Delete Italian translation');
|
||||
$this->assertStringEndsWith('/kittens', $this->getSession()->getCurrentUrl());
|
||||
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertFalse($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->wasDefaultRevision());
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->assertLessThan($entity->getRevisionId(), $it_revision->getRevisionId());
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString());
|
||||
$this->assertSession()->linkByHrefExists($add_translation_href);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\Core\Field\Entity\BaseFieldOverride;
|
||||
use Drupal\Core\Language\Language;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\language\Entity\ContentLanguageSettings;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
|
||||
|
||||
/**
|
||||
* Tests the content translation settings UI.
|
||||
*
|
||||
* @covers \Drupal\language\Form\ContentLanguageSettingsForm
|
||||
* @covers ::_content_translation_form_language_content_settings_form_alter
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationSettingsTest extends BrowserTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
use FieldUiTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'node',
|
||||
'comment',
|
||||
'field_ui',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Set up two content types to test fields shared between different
|
||||
// bundles.
|
||||
$this->drupalCreateContentType(['type' => 'article']);
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$this->addDefaultCommentField('node', 'article', 'comment_article', CommentItemInterface::OPEN, 'comment_article');
|
||||
$this->addDefaultCommentField('node', 'page', 'comment_page');
|
||||
|
||||
$admin_user = $this->drupalCreateUser([
|
||||
'access administration pages',
|
||||
'administer languages',
|
||||
'administer content translation',
|
||||
'administer content types',
|
||||
'administer node fields',
|
||||
'administer comment fields',
|
||||
'administer comments',
|
||||
'administer comment types',
|
||||
'administer account settings',
|
||||
]);
|
||||
$this->drupalLogin($admin_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the settings UI works as expected.
|
||||
*/
|
||||
public function testSettingsUI(): void {
|
||||
// Check for the content_translation_menu_links_discovered_alter() changes.
|
||||
$this->drupalGet('admin/config');
|
||||
$this->assertSession()->linkExists('Content language and translation');
|
||||
$this->assertSession()->pageTextContains('Configure language and translation support for content.');
|
||||
// Test that the translation settings are ignored if the bundle is marked
|
||||
// translatable but the entity type is not.
|
||||
$edit = ['settings[comment][comment_article][translatable]' => TRUE];
|
||||
$this->assertSettings('comment', 'comment_article', FALSE, $edit);
|
||||
|
||||
// Test that the translation settings are ignored if only a field is marked
|
||||
// as translatable and not the related entity type and bundle.
|
||||
$edit = ['settings[comment][comment_article][fields][comment_body]' => TRUE];
|
||||
$this->assertSettings('comment', 'comment_article', FALSE, $edit);
|
||||
|
||||
// Test that the translation settings are not stored if an entity type and
|
||||
// bundle are marked as translatable but no field is.
|
||||
$edit = [
|
||||
'entity_types[comment]' => TRUE,
|
||||
'settings[comment][comment_article][translatable]' => TRUE,
|
||||
// Base fields are translatable by default.
|
||||
'settings[comment][comment_article][fields][changed]' => FALSE,
|
||||
'settings[comment][comment_article][fields][created]' => FALSE,
|
||||
'settings[comment][comment_article][fields][homepage]' => FALSE,
|
||||
'settings[comment][comment_article][fields][hostname]' => FALSE,
|
||||
'settings[comment][comment_article][fields][mail]' => FALSE,
|
||||
'settings[comment][comment_article][fields][name]' => FALSE,
|
||||
'settings[comment][comment_article][fields][status]' => FALSE,
|
||||
'settings[comment][comment_article][fields][subject]' => FALSE,
|
||||
'settings[comment][comment_article][fields][uid]' => FALSE,
|
||||
];
|
||||
$this->assertSettings('comment', 'comment_article', FALSE, $edit);
|
||||
$this->assertSession()->statusMessageContains('At least one field needs to be translatable to enable Comment_article for translation.', 'error');
|
||||
|
||||
// Test that the translation settings are not stored if a non-configurable
|
||||
// language is set as default and the language selector is hidden.
|
||||
$edit = [
|
||||
'entity_types[comment]' => TRUE,
|
||||
'settings[comment][comment_article][settings][language][langcode]' => Language::LANGCODE_NOT_SPECIFIED,
|
||||
'settings[comment][comment_article][settings][language][language_alterable]' => FALSE,
|
||||
'settings[comment][comment_article][translatable]' => TRUE,
|
||||
'settings[comment][comment_article][fields][comment_body]' => TRUE,
|
||||
];
|
||||
$this->assertSettings('comment', 'comment_article', FALSE, $edit);
|
||||
$this->assertSession()->statusMessageContains('Translation is not supported if language is always one of: Not specified, Not applicable', 'error');
|
||||
|
||||
// Test that a field shared among different bundles can be enabled without
|
||||
// needing to make all the related bundles translatable.
|
||||
$edit = [
|
||||
'entity_types[comment]' => TRUE,
|
||||
'settings[comment][comment_article][settings][language][langcode]' => 'current_interface',
|
||||
'settings[comment][comment_article][settings][language][language_alterable]' => TRUE,
|
||||
'settings[comment][comment_article][translatable]' => TRUE,
|
||||
'settings[comment][comment_article][fields][comment_body]' => TRUE,
|
||||
// Override both comment subject fields to untranslatable.
|
||||
'settings[comment][comment_article][fields][subject]' => FALSE,
|
||||
'settings[comment][comment][fields][subject]' => FALSE,
|
||||
];
|
||||
$this->assertSettings('comment', 'comment_article', TRUE, $edit);
|
||||
$entity_field_manager = \Drupal::service('entity_field.manager');
|
||||
$definition = $entity_field_manager->getFieldDefinitions('comment', 'comment_article')['comment_body'];
|
||||
$this->assertTrue($definition->isTranslatable(), 'Article comment body is translatable.');
|
||||
$definition = $entity_field_manager->getFieldDefinitions('comment', 'comment_article')['subject'];
|
||||
$this->assertFalse($definition->isTranslatable(), 'Article comment subject is not translatable.');
|
||||
|
||||
$definition = $entity_field_manager->getFieldDefinitions('comment', 'comment')['comment_body'];
|
||||
$this->assertFalse($definition->isTranslatable(), 'Page comment body is not translatable.');
|
||||
$definition = $entity_field_manager->getFieldDefinitions('comment', 'comment')['subject'];
|
||||
$this->assertFalse($definition->isTranslatable(), 'Page comment subject is not translatable.');
|
||||
|
||||
// Test that translation can be enabled for base fields.
|
||||
$edit = [
|
||||
'entity_types[entity_test_mul]' => TRUE,
|
||||
'settings[entity_test_mul][entity_test_mul][translatable]' => TRUE,
|
||||
'settings[entity_test_mul][entity_test_mul][fields][name]' => TRUE,
|
||||
'settings[entity_test_mul][entity_test_mul][fields][user_id]' => FALSE,
|
||||
];
|
||||
$this->assertSettings('entity_test_mul', 'entity_test_mul', TRUE, $edit);
|
||||
$field_override = BaseFieldOverride::loadByName('entity_test_mul', 'entity_test_mul', 'name');
|
||||
$this->assertTrue($field_override->isTranslatable(), 'Base fields can be overridden with a base field bundle override entity.');
|
||||
$definitions = $entity_field_manager->getFieldDefinitions('entity_test_mul', 'entity_test_mul');
|
||||
$this->assertTrue($definitions['name']->isTranslatable());
|
||||
$this->assertFalse($definitions['user_id']->isTranslatable());
|
||||
|
||||
// Test that language settings are correctly stored.
|
||||
$language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('comment', 'comment_article');
|
||||
$this->assertEquals('current_interface', $language_configuration->getDefaultLangcode(), 'The default language for article comments is set to the interface text language selected for page.');
|
||||
$this->assertTrue($language_configuration->isLanguageAlterable(), 'The language selector for article comments is shown.');
|
||||
|
||||
// Verify language widget appears on comment type form.
|
||||
$this->drupalGet('admin/structure/comment/manage/comment_article');
|
||||
$this->assertSession()->fieldExists('language_configuration[content_translation]');
|
||||
$this->assertSession()->checkboxChecked('edit-language-configuration-content-translation');
|
||||
|
||||
// Verify that translation may be enabled for the article content type.
|
||||
$edit = [
|
||||
'language_configuration[content_translation]' => TRUE,
|
||||
];
|
||||
// Make sure the checkbox is available and not checked by default.
|
||||
$this->drupalGet('admin/structure/types/manage/article');
|
||||
$this->assertSession()->fieldExists('language_configuration[content_translation]');
|
||||
$this->assertSession()->checkboxNotChecked('edit-language-configuration-content-translation');
|
||||
$this->drupalGet('admin/structure/types/manage/article');
|
||||
$this->submitForm($edit, 'Save');
|
||||
$this->drupalGet('admin/structure/types/manage/article');
|
||||
$this->assertSession()->checkboxChecked('edit-language-configuration-content-translation');
|
||||
|
||||
// Test that the title field of nodes is available in the settings form.
|
||||
$edit = [
|
||||
'entity_types[node]' => TRUE,
|
||||
'settings[node][article][settings][language][langcode]' => 'current_interface',
|
||||
'settings[node][article][settings][language][language_alterable]' => TRUE,
|
||||
'settings[node][article][translatable]' => TRUE,
|
||||
'settings[node][article][fields][title]' => TRUE,
|
||||
];
|
||||
$this->assertSettings('node', 'article', TRUE, $edit);
|
||||
|
||||
foreach ([TRUE, FALSE] as $translatable) {
|
||||
// Test that configurable field translatability is correctly switched.
|
||||
$edit = ['settings[node][article][fields][body]' => $translatable];
|
||||
$this->assertSettings('node', 'article', TRUE, $edit);
|
||||
$field = FieldConfig::loadByName('node', 'article', 'body');
|
||||
$definitions = $entity_field_manager->getFieldDefinitions('node', 'article');
|
||||
$this->assertEquals($translatable, $definitions['body']->isTranslatable(), 'Field translatability correctly switched.');
|
||||
$this->assertEquals($definitions['body']->isTranslatable(), $field->isTranslatable(), 'Configurable field translatability correctly switched.');
|
||||
|
||||
// Test that also the Field UI form behaves correctly.
|
||||
$translatable = !$translatable;
|
||||
$edit = ['translatable' => $translatable];
|
||||
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.body');
|
||||
$this->submitForm($edit, 'Save settings');
|
||||
$entity_field_manager->clearCachedFieldDefinitions();
|
||||
$field = FieldConfig::loadByName('node', 'article', 'body');
|
||||
$definitions = $entity_field_manager->getFieldDefinitions('node', 'article');
|
||||
$this->assertEquals($translatable, $definitions['body']->isTranslatable(), 'Field translatability correctly switched.');
|
||||
$this->assertEquals($definitions['body']->isTranslatable(), $field->isTranslatable(), 'Configurable field translatability correctly switched.');
|
||||
}
|
||||
|
||||
// Test that we can't use the 'Not specified' default language when it is
|
||||
// not showing in the language selector.
|
||||
$edit = [
|
||||
'language_configuration[langcode]' => 'und',
|
||||
'language_configuration[language_alterable]' => FALSE,
|
||||
'language_configuration[content_translation]' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/structure/types/manage/article');
|
||||
$this->submitForm($edit, 'Save');
|
||||
$this->getSession()->getPage()->hasContent('"Show language selector" is not compatible with translating content that has default language: und. Either do not hide the language selector or pick a specific language.');
|
||||
|
||||
// Test that the order of the language list is similar to other language
|
||||
// lists, such as in Views UI.
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
|
||||
$expected_elements = [
|
||||
'site_default',
|
||||
'current_interface',
|
||||
'authors_default',
|
||||
'en',
|
||||
'und',
|
||||
'zxx',
|
||||
];
|
||||
$options = $this->assertSession()->selectExists('edit-settings-node-article-settings-language-langcode')->findAll('css', 'option');
|
||||
$options = array_map(function ($item) {
|
||||
return $item->getValue();
|
||||
}, $options);
|
||||
$this->assertSame($expected_elements, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the language settings checkbox on account settings page.
|
||||
*/
|
||||
public function testAccountLanguageSettingsUI(): void {
|
||||
// Make sure the checkbox is available and not checked by default.
|
||||
$this->drupalGet('admin/config/people/accounts');
|
||||
$this->assertSession()->fieldExists('language[content_translation]');
|
||||
$this->assertSession()->checkboxNotChecked('edit-language-content-translation');
|
||||
|
||||
$edit = [
|
||||
'language[content_translation]' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/config/people/accounts');
|
||||
$this->submitForm($edit, 'Save configuration');
|
||||
$this->drupalGet('admin/config/people/accounts');
|
||||
$this->assertSession()->checkboxChecked('edit-language-content-translation');
|
||||
|
||||
// Make sure account settings can be saved.
|
||||
$this->drupalGet('admin/config/people/accounts');
|
||||
$this->submitForm(['anonymous' => 'Save me!'], 'Save configuration');
|
||||
$this->assertSession()->fieldValueEquals('anonymous', 'Save me!');
|
||||
$this->assertSession()->statusMessageContains('The configuration options have been saved.', 'status');
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that translatability has the expected value for the given bundle.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* The entity type for which to check translatability.
|
||||
* @param string|null $bundle
|
||||
* The bundle for which to check translatability.
|
||||
* @param bool $enabled
|
||||
* TRUE if translatability should be enabled, FALSE otherwise.
|
||||
* @param array $edit
|
||||
* An array of values to submit to the content translation settings page.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertSettings(string $entity_type, ?string $bundle, bool $enabled, array $edit): void {
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
$this->submitForm($edit, 'Save configuration');
|
||||
$status = $enabled ? 'enabled' : 'disabled';
|
||||
$message = "Translation for entity $entity_type ($bundle) is $status.";
|
||||
$this->assertEquals($enabled, \Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that field setting depends on bundle translatability.
|
||||
*/
|
||||
public function testFieldTranslatableSettingsUI(): void {
|
||||
// At least one field needs to be translatable to enable article for
|
||||
// translation. Create an extra field to be used for this purpose. We use
|
||||
// the UI to test our form alterations.
|
||||
$this->fieldUIAddNewField('admin/structure/types/manage/article', 'article_text', 'Test', 'text');
|
||||
|
||||
// Tests that field doesn't have translatable setting if bundle is not
|
||||
// translatable.
|
||||
$path = 'admin/structure/types/manage/article/fields/node.article.field_article_text';
|
||||
$this->drupalGet($path);
|
||||
$this->assertSession()->fieldDisabled('edit-translatable');
|
||||
$this->assertSession()->pageTextContains('To configure translation for this field, enable language support for this type.');
|
||||
|
||||
// 'Users may translate this field' should be unchecked by default.
|
||||
$this->assertSession()->checkboxNotChecked('translatable');
|
||||
|
||||
// Tests that field has translatable setting if bundle is translatable.
|
||||
// Note: this field is not translatable when enable bundle translatability.
|
||||
$edit = [
|
||||
'entity_types[node]' => TRUE,
|
||||
'settings[node][article][settings][language][language_alterable]' => TRUE,
|
||||
'settings[node][article][translatable]' => TRUE,
|
||||
'settings[node][article][fields][field_article_text]' => TRUE,
|
||||
];
|
||||
$this->assertSettings('node', 'article', TRUE, $edit);
|
||||
$this->drupalGet($path);
|
||||
$this->assertSession()->fieldEnabled('edit-translatable');
|
||||
$this->assertSession()->checkboxChecked('edit-translatable');
|
||||
$this->assertSession()->pageTextNotContains('To enable translation of this field, enable language support for this type.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the translatable settings checkbox for untranslatable entities.
|
||||
*/
|
||||
public function testNonTranslatableTranslationSettingsUI(): void {
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
$this->assertSession()->fieldNotExists('settings[entity_test][entity_test][translatable]');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\block_content\Entity\BlockContentType;
|
||||
use Drupal\comment\Entity\CommentType;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the Content translation settings.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationStandardFieldsTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'node',
|
||||
'comment',
|
||||
'field_ui',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $profile = 'testing';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$admin_user = $this->drupalCreateUser([
|
||||
'access administration pages',
|
||||
'administer languages',
|
||||
'administer content translation',
|
||||
'administer content types',
|
||||
'administer node fields',
|
||||
'administer comment fields',
|
||||
'administer comments',
|
||||
'administer comment types',
|
||||
]);
|
||||
$this->drupalLogin($admin_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that translatable fields are being rendered.
|
||||
*/
|
||||
public function testFieldTranslatableArticle(): void {
|
||||
// Install block and field modules.
|
||||
\Drupal::service('module_installer')->install(
|
||||
[
|
||||
'block',
|
||||
'block_content',
|
||||
'filter',
|
||||
'image',
|
||||
'text',
|
||||
]);
|
||||
|
||||
// Create a basic block type with a body field.
|
||||
$bundle = BlockContentType::create([
|
||||
'id' => 'basic',
|
||||
'label' => 'Basic',
|
||||
'revision' => FALSE,
|
||||
]);
|
||||
$bundle->save();
|
||||
block_content_add_body_field($bundle->id());
|
||||
|
||||
// Create a comment type with a body field.
|
||||
$bundle = CommentType::create([
|
||||
'id' => 'comment',
|
||||
'label' => 'Comment',
|
||||
'target_entity_type_id' => 'node',
|
||||
]);
|
||||
$bundle->save();
|
||||
\Drupal::service('comment.manager')->addBodyField('comment');
|
||||
|
||||
// Create the article content type and add a comment, image and tag field.
|
||||
$this->drupalCreateContentType(['type' => 'article', 'title' => 'Article']);
|
||||
|
||||
$entity_type_manager = \Drupal::entityTypeManager();
|
||||
$entity_type_manager->getStorage('field_storage_config')->create([
|
||||
'entity_type' => 'node',
|
||||
'field_name' => 'comment',
|
||||
'type' => 'text',
|
||||
])->save();
|
||||
|
||||
$entity_type_manager->getStorage('field_config')->create([
|
||||
'label' => 'Comments',
|
||||
'field_name' => 'comment',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'article',
|
||||
])->save();
|
||||
|
||||
$entity_type_manager->getStorage('field_storage_config')->create([
|
||||
'entity_type' => 'node',
|
||||
'field_name' => 'field_image',
|
||||
'type' => 'image',
|
||||
])->save();
|
||||
|
||||
$entity_type_manager->getStorage('field_config')->create([
|
||||
'label' => 'Image',
|
||||
'field_name' => 'field_image',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'article',
|
||||
])->save();
|
||||
|
||||
$entity_type_manager->getStorage('field_storage_config')->create([
|
||||
'entity_type' => 'node',
|
||||
'field_name' => 'field_tags',
|
||||
'type' => 'text',
|
||||
])->save();
|
||||
|
||||
$entity_type_manager->getStorage('field_config')->create([
|
||||
'label' => 'Tags',
|
||||
'field_name' => 'field_tags',
|
||||
'entity_type' => 'node',
|
||||
'bundle' => 'article',
|
||||
])->save();
|
||||
|
||||
$entity_type_manager->getStorage('field_storage_config')->create([
|
||||
'entity_type' => 'user',
|
||||
'field_name' => 'user_picture',
|
||||
'type' => 'image',
|
||||
])->save();
|
||||
|
||||
// Add a user picture field to the user entity.
|
||||
$entity_type_manager->getStorage('field_config')->create([
|
||||
'label' => 'Tags',
|
||||
'field_name' => 'user_picture',
|
||||
'entity_type' => 'user',
|
||||
'bundle' => 'user',
|
||||
])->save();
|
||||
|
||||
$path = 'admin/config/regional/content-language';
|
||||
$this->drupalGet($path);
|
||||
|
||||
// Check content block fields.
|
||||
$this->assertSession()->checkboxChecked('edit-settings-block-content-basic-fields-body');
|
||||
|
||||
// Check comment fields.
|
||||
$this->assertSession()->checkboxChecked('edit-settings-comment-comment-fields-comment-body');
|
||||
|
||||
// Check node fields.
|
||||
$this->assertSession()->checkboxChecked('edit-settings-node-article-fields-comment');
|
||||
$this->assertSession()->checkboxChecked('edit-settings-node-article-fields-field-image');
|
||||
$this->assertSession()->checkboxChecked('edit-settings-node-article-fields-field-tags');
|
||||
|
||||
// Check user fields.
|
||||
$this->assertSession()->checkboxChecked('edit-settings-user-user-fields-user-picture');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that revision_log is not translatable.
|
||||
*/
|
||||
public function testRevisionLogNotTranslatable(): void {
|
||||
$path = 'admin/config/regional/content-language';
|
||||
$this->drupalGet($path);
|
||||
$this->assertSession()->fieldNotExists('edit-settings-node-article-fields-revision-log');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* Tests the field synchronization behavior for the image field.
|
||||
*
|
||||
* @covers ::_content_translation_form_language_content_settings_form_alter
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationSyncImageTest extends ContentTranslationTestBase {
|
||||
|
||||
use TestFileCreationTrait {
|
||||
getTestFiles as drupalGetTestFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The cardinality of the image field.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $cardinality;
|
||||
|
||||
/**
|
||||
* The test image files.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'entity_test',
|
||||
'image',
|
||||
'field_ui',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
$this->files = $this->drupalGetTestFiles('image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the test image field.
|
||||
*/
|
||||
protected function setupTestFields(): void {
|
||||
$this->fieldName = 'field_test_et_ui_image';
|
||||
$this->cardinality = 3;
|
||||
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => $this->fieldName,
|
||||
'entity_type' => $this->entityTypeId,
|
||||
'type' => 'image',
|
||||
'cardinality' => $this->cardinality,
|
||||
])->save();
|
||||
|
||||
FieldConfig::create([
|
||||
'entity_type' => $this->entityTypeId,
|
||||
'field_name' => $this->fieldName,
|
||||
'bundle' => $this->entityTypeId,
|
||||
'label' => 'Test translatable image field',
|
||||
'third_party_settings' => [
|
||||
'content_translation' => [
|
||||
'translation_sync' => [
|
||||
'file' => FALSE,
|
||||
'alt' => 'alt',
|
||||
'title' => 'title',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditorPermissions(): array {
|
||||
// Every entity-type-specific test needs to define these.
|
||||
return ['administer entity_test_mul fields', 'administer languages', 'administer content translation'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests image field synchronization.
|
||||
*/
|
||||
public function testImageFieldSync(): void {
|
||||
// Check that the alt and title fields are enabled for the image field.
|
||||
$this->drupalLogin($this->editor);
|
||||
$this->drupalGet('entity_test_mul/structure/' . $this->entityTypeId . '/fields/' . $this->entityTypeId . '.' . $this->entityTypeId . '.' . $this->fieldName);
|
||||
$this->assertSession()->checkboxChecked('edit-third-party-settings-content-translation-translation-sync-alt');
|
||||
$this->assertSession()->checkboxChecked('edit-third-party-settings-content-translation-translation-sync-title');
|
||||
$edit = [
|
||||
'third_party_settings[content_translation][translation_sync][alt]' => FALSE,
|
||||
'third_party_settings[content_translation][translation_sync][title]' => FALSE,
|
||||
];
|
||||
$this->submitForm($edit, 'Save settings');
|
||||
|
||||
// Check that the content translation settings page reflects the changes
|
||||
// performed in the field edit page.
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
$this->assertSession()->checkboxNotChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-alt');
|
||||
$this->assertSession()->checkboxNotChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-title');
|
||||
$edit = [
|
||||
'settings[entity_test_mul][entity_test_mul][fields][field_test_et_ui_image]' => TRUE,
|
||||
'settings[entity_test_mul][entity_test_mul][columns][field_test_et_ui_image][alt]' => TRUE,
|
||||
'settings[entity_test_mul][entity_test_mul][columns][field_test_et_ui_image][title]' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
$this->submitForm($edit, 'Save configuration');
|
||||
$this->assertSession()->statusMessageNotExists('error');
|
||||
$this->assertSession()->checkboxChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-alt');
|
||||
$this->assertSession()->checkboxChecked('edit-settings-entity-test-mul-entity-test-mul-columns-field-test-et-ui-image-title');
|
||||
$this->drupalLogin($this->translator);
|
||||
|
||||
$default_langcode = $this->langcodes[0];
|
||||
$langcode = $this->langcodes[1];
|
||||
|
||||
// Populate the test entity with some random initial values.
|
||||
$values = [
|
||||
'name' => $this->randomMachineName(),
|
||||
'user_id' => 2,
|
||||
'langcode' => $default_langcode,
|
||||
];
|
||||
$entity = \Drupal::entityTypeManager()
|
||||
->getStorage($this->entityTypeId)
|
||||
->create($values);
|
||||
|
||||
// Create some file entities from the generated test files and store them.
|
||||
$values = [];
|
||||
for ($delta = 0; $delta < $this->cardinality; $delta++) {
|
||||
// For the default language use the same order for files and field items.
|
||||
$index = $delta;
|
||||
|
||||
// Create the file entity for the image being processed and record its
|
||||
// identifier.
|
||||
$field_values = [
|
||||
'uri' => $this->files[$index]->uri,
|
||||
'uid' => \Drupal::currentUser()->id(),
|
||||
];
|
||||
$file = File::create($field_values);
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
$fid = $file->id();
|
||||
$this->files[$index]->fid = $fid;
|
||||
|
||||
// Generate the item for the current image file entity and attach it to
|
||||
// the entity.
|
||||
$item = [
|
||||
'target_id' => $fid,
|
||||
'alt' => $default_langcode . '_' . $fid . '_' . $this->randomMachineName(),
|
||||
'title' => $default_langcode . '_' . $fid . '_' . $this->randomMachineName(),
|
||||
];
|
||||
$entity->{$this->fieldName}[] = $item;
|
||||
|
||||
// Store the generated values keying them by fid for easier lookup.
|
||||
$values[$default_langcode][$fid] = $item;
|
||||
}
|
||||
$entity = $this->saveEntity($entity);
|
||||
|
||||
// Create some field translations for the test image field. The translated
|
||||
// items will be one less than the original values to check that only the
|
||||
// translated ones will be preserved. In fact we want the same fids and
|
||||
// items order for both languages.
|
||||
$translation = $entity->addTranslation($langcode);
|
||||
for ($delta = 0; $delta < $this->cardinality - 1; $delta++) {
|
||||
// Simulate a field reordering: items are shifted of one position ahead.
|
||||
// The modulo operator ensures we start from the beginning after reaching
|
||||
// the maximum allowed delta.
|
||||
$index = ($delta + 1) % $this->cardinality;
|
||||
|
||||
// Generate the item for the current image file entity and attach it to
|
||||
// the entity.
|
||||
$fid = $this->files[$index]->fid;
|
||||
$item = [
|
||||
'target_id' => $fid,
|
||||
'alt' => $langcode . '_' . $fid . '_' . $this->randomMachineName(),
|
||||
'title' => $langcode . '_' . $fid . '_' . $this->randomMachineName(),
|
||||
];
|
||||
$translation->{$this->fieldName}[] = $item;
|
||||
|
||||
// Again store the generated values keying them by fid for easier lookup.
|
||||
$values[$langcode][$fid] = $item;
|
||||
}
|
||||
|
||||
// Perform synchronization: the translation language is used as source,
|
||||
// while the default language is used as target.
|
||||
$this->manager->getTranslationMetadata($translation)->setSource($default_langcode);
|
||||
$entity = $this->saveEntity($translation);
|
||||
$translation = $entity->getTranslation($langcode);
|
||||
|
||||
// Check that one value has been dropped from the original values.
|
||||
$assert = count($entity->{$this->fieldName}) == 2;
|
||||
$this->assertTrue($assert, 'One item correctly removed from the synchronized field values.');
|
||||
|
||||
// Check that fids have been synchronized and translatable column values
|
||||
// have been retained.
|
||||
$fids = [];
|
||||
foreach ($entity->{$this->fieldName} as $delta => $item) {
|
||||
$value = $values[$default_langcode][$item->target_id];
|
||||
$source_item = $translation->{$this->fieldName}->get($delta);
|
||||
$assert = $item->target_id == $source_item->target_id && $item->alt == $value['alt'] && $item->title == $value['title'];
|
||||
$this->assertTrue($assert, "Field item $item->target_id has been successfully synchronized.");
|
||||
$fids[$item->target_id] = TRUE;
|
||||
}
|
||||
|
||||
// Check that the dropped value is the right one.
|
||||
$removed_fid = $this->files[0]->fid;
|
||||
$this->assertTrue(!isset($fids[$removed_fid]), "Field item $removed_fid has been correctly removed.");
|
||||
|
||||
// Add back an item for the dropped value and perform synchronization again.
|
||||
$values[$langcode][$removed_fid] = [
|
||||
'target_id' => $removed_fid,
|
||||
'alt' => $langcode . '_' . $removed_fid . '_' . $this->randomMachineName(),
|
||||
'title' => $langcode . '_' . $removed_fid . '_' . $this->randomMachineName(),
|
||||
];
|
||||
$translation->{$this->fieldName}->setValue(array_values($values[$langcode]));
|
||||
$entity = $this->saveEntity($translation);
|
||||
$translation = $entity->getTranslation($langcode);
|
||||
|
||||
// Check that the value has been added to the default language.
|
||||
$assert = count($entity->{$this->fieldName}->getValue()) == 3;
|
||||
$this->assertTrue($assert, 'One item correctly added to the synchronized field values.');
|
||||
|
||||
foreach ($entity->{$this->fieldName} as $delta => $item) {
|
||||
// When adding an item its value is copied over all the target languages,
|
||||
// thus in this case the source language needs to be used to check the
|
||||
// values instead of the target one.
|
||||
$fid_langcode = $item->target_id != $removed_fid ? $default_langcode : $langcode;
|
||||
$value = $values[$fid_langcode][$item->target_id];
|
||||
$source_item = $translation->{$this->fieldName}->get($delta);
|
||||
$assert = $item->target_id == $source_item->target_id && $item->alt == $value['alt'] && $item->title == $value['title'];
|
||||
$this->assertTrue($assert, "Field item $item->target_id has been successfully synchronized.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the passed entity and reloads it, enabling compatibility mode.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to be saved.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The saved entity.
|
||||
*/
|
||||
protected function saveEntity(EntityInterface $entity): EntityInterface {
|
||||
$entity->save();
|
||||
$entity = \Drupal::entityTypeManager()->getStorage('entity_test_mul')->loadUnchanged($entity->id());
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
|
||||
/**
|
||||
* Base class for content translation tests.
|
||||
*/
|
||||
abstract class ContentTranslationTestBase extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['text'];
|
||||
|
||||
/**
|
||||
* The entity type being tested.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId = 'entity_test_mul';
|
||||
|
||||
/**
|
||||
* The bundle being tested.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle;
|
||||
|
||||
/**
|
||||
* The added languages.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $langcodes;
|
||||
|
||||
/**
|
||||
* The account to be used to test translation operations.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* The account to be used to test multilingual entity editing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $editor;
|
||||
|
||||
/**
|
||||
* The account to be used to test access to both workflows.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $administrator;
|
||||
|
||||
/**
|
||||
* The name of the field used to test translation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldName;
|
||||
|
||||
/**
|
||||
* The translation controller for the current entity type.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationHandlerInterface
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
/**
|
||||
* Completes preparation for content translation tests.
|
||||
*/
|
||||
protected function doSetup(): void {
|
||||
$this->setupLanguages();
|
||||
$this->setupBundle();
|
||||
$this->enableTranslation();
|
||||
$this->setupUsers();
|
||||
$this->setupTestFields();
|
||||
|
||||
$this->manager = $this->container->get('content_translation.manager');
|
||||
$this->controller = $this->manager->getTranslationHandler($this->entityTypeId);
|
||||
|
||||
// Rebuild the container so that the new languages are picked up by services
|
||||
// that hold a list of languages.
|
||||
$this->rebuildContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional languages.
|
||||
*/
|
||||
protected function setupLanguages() {
|
||||
$this->langcodes = ['it', 'fr'];
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
ConfigurableLanguage::createFromLangcode($langcode)->save();
|
||||
}
|
||||
array_unshift($this->langcodes, \Drupal::languageManager()->getDefaultLanguage()->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of permissions needed for the translator.
|
||||
*/
|
||||
protected function getTranslatorPermissions() {
|
||||
return array_filter([$this->getTranslatePermission(), 'create content translations', 'update content translations', 'delete content translations']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translate permissions for the current entity and bundle.
|
||||
*/
|
||||
protected function getTranslatePermission() {
|
||||
$entity_type = \Drupal::entityTypeManager()->getDefinition($this->entityTypeId);
|
||||
if ($permission_granularity = $entity_type->getPermissionGranularity()) {
|
||||
return $permission_granularity == 'bundle' ? "translate {$this->bundle} {$this->entityTypeId}" : "translate {$this->entityTypeId}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of permissions needed for the editor.
|
||||
*/
|
||||
protected function getEditorPermissions() {
|
||||
// Every entity-type-specific test needs to define these.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of permissions needed for the administrator.
|
||||
*/
|
||||
protected function getAdministratorPermissions() {
|
||||
return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), ['administer languages', 'administer content translation']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and activates translator, editor and admin users.
|
||||
*/
|
||||
protected function setupUsers() {
|
||||
$this->translator = $this->drupalCreateUser($this->getTranslatorPermissions(), 'translator');
|
||||
$this->editor = $this->drupalCreateUser($this->getEditorPermissions(), 'editor');
|
||||
$this->administrator = $this->drupalCreateUser($this->getAdministratorPermissions(), 'administrator');
|
||||
$this->drupalLogin($this->translator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or initializes the bundle date if needed.
|
||||
*/
|
||||
protected function setupBundle() {
|
||||
if (empty($this->bundle)) {
|
||||
$this->bundle = $this->entityTypeId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables translation for the current entity type and bundle.
|
||||
*/
|
||||
protected function enableTranslation() {
|
||||
// Enable translation for the current entity type and ensure the change is
|
||||
// picked up.
|
||||
\Drupal::service('content_translation.manager')->setEnabled($this->entityTypeId, $this->bundle, TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the test fields.
|
||||
*/
|
||||
protected function setupTestFields() {
|
||||
if (empty($this->fieldName)) {
|
||||
$this->fieldName = 'field_test_et_ui_test';
|
||||
}
|
||||
FieldStorageConfig::create([
|
||||
'field_name' => $this->fieldName,
|
||||
'type' => 'string',
|
||||
'entity_type' => $this->entityTypeId,
|
||||
'cardinality' => 1,
|
||||
])->save();
|
||||
FieldConfig::create([
|
||||
'entity_type' => $this->entityTypeId,
|
||||
'field_name' => $this->fieldName,
|
||||
'bundle' => $this->bundle,
|
||||
'label' => 'Test translatable text-field',
|
||||
])->save();
|
||||
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
|
||||
$display_repository = \Drupal::service('entity_display.repository');
|
||||
$display_repository->getFormDisplay($this->entityTypeId, $this->bundle, 'default')
|
||||
->setComponent($this->fieldName, [
|
||||
'type' => 'string_textfield',
|
||||
'weight' => 0,
|
||||
])
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity to be translated.
|
||||
*
|
||||
* @param array $values
|
||||
* An array of initial values for the entity.
|
||||
* @param string $langcode
|
||||
* The initial language code of the entity.
|
||||
* @param string $bundle_name
|
||||
* (optional) The entity bundle, if the entity uses bundles. Defaults to
|
||||
* NULL. If left NULL, $this->bundle will be used.
|
||||
*
|
||||
* @return string
|
||||
* The entity id.
|
||||
*/
|
||||
protected function createEntity($values, $langcode, $bundle_name = NULL) {
|
||||
$entity_values = $values;
|
||||
$entity_values['langcode'] = $langcode;
|
||||
$entity_type = \Drupal::entityTypeManager()->getDefinition($this->entityTypeId);
|
||||
if ($bundle_key = $entity_type->getKey('bundle')) {
|
||||
$entity_values[$bundle_key] = $bundle_name ?: $this->bundle;
|
||||
}
|
||||
$storage = $this->container->get('entity_type.manager')->getStorage($this->entityTypeId);
|
||||
if (!($storage instanceof SqlContentEntityStorage)) {
|
||||
foreach ($values as $property => $value) {
|
||||
if (is_array($value)) {
|
||||
$entity_values[$property] = [$langcode => $value];
|
||||
}
|
||||
}
|
||||
}
|
||||
$entity = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId)
|
||||
->create($entity_values);
|
||||
$entity->save();
|
||||
return $entity->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the edit URL for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being edited.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The edit URL.
|
||||
*/
|
||||
protected function getEditUrl(ContentEntityInterface $entity) {
|
||||
if ($entity->access('update', $this->loggedInUser)) {
|
||||
$url = $entity->toUrl('edit-form');
|
||||
}
|
||||
else {
|
||||
$url = $entity->toUrl('drupal:content-translation-edit');
|
||||
$url->setRouteParameter('language', $entity->language()->getId());
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the content translation UI check skip.
|
||||
*
|
||||
* @covers \Drupal\language\Form\ContentLanguageSettingsForm
|
||||
* @covers ::_content_translation_form_language_content_settings_form_alter
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationUISkipTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['content_translation_test', 'user', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Tests the content_translation_ui_skip key functionality.
|
||||
*/
|
||||
public function testUICheckSkip(): void {
|
||||
$admin_user = $this->drupalCreateUser([
|
||||
'translate any entity',
|
||||
'administer content translation',
|
||||
'administer languages',
|
||||
]);
|
||||
$this->drupalLogin($admin_user);
|
||||
// Visit the content translation.
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
|
||||
// Check the message regarding UI integration.
|
||||
$this->assertSession()->pageTextContains('Test entity - Translatable skip UI check');
|
||||
$this->assertSession()->pageTextContains('Test entity - Translatable check UI (Translation is not supported)');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,633 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Entity\EntityChangedInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Language\Language;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
use Drupal\user\UserInterface;
|
||||
|
||||
/**
|
||||
* Tests the Content Translation UI.
|
||||
*/
|
||||
abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
|
||||
|
||||
use AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* The id of the entity being translated.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $entityId;
|
||||
|
||||
/**
|
||||
* Whether the behavior of the language selector should be tested.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $testLanguageSelector = TRUE;
|
||||
|
||||
/**
|
||||
* Flag to determine if "all languages" rendering is tested.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $testHTMLEscapeForAllLanguages = FALSE;
|
||||
|
||||
/**
|
||||
* Default cache contexts expected on a non-translated entity.
|
||||
*
|
||||
* Cache contexts will not be checked if this list is empty.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $defaultCacheContexts = ['languages:language_interface', 'theme', 'url.query_args:_wrapper_format', 'user.permissions'];
|
||||
|
||||
/**
|
||||
* Tests the basic translation UI.
|
||||
*/
|
||||
public function testTranslationUI(): void {
|
||||
$this->doTestBasicTranslation();
|
||||
$this->doTestTranslationOverview();
|
||||
$this->doTestOutdatedStatus();
|
||||
$this->doTestPublishedStatus();
|
||||
$this->doTestAuthoringInfo();
|
||||
$this->doTestTranslationEdit();
|
||||
$this->doTestTranslationChanged();
|
||||
$this->doTestChangedTimeAfterSaveWithoutChanges();
|
||||
$this->doTestTranslationDeletion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the basic translation workflow.
|
||||
*/
|
||||
protected function doTestBasicTranslation() {
|
||||
// Create a new test entity with original values in the default language.
|
||||
$default_langcode = $this->langcodes[0];
|
||||
$values[$default_langcode] = $this->getNewEntityValues($default_langcode);
|
||||
// Create the entity with the editor as owner, so that afterwards a new
|
||||
// translation is created by the translator and the translation author is
|
||||
// tested.
|
||||
$this->drupalLogin($this->editor);
|
||||
$this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
|
||||
$this->drupalLogin($this->translator);
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->assertNotEmpty($entity, 'Entity found in the database.');
|
||||
$this->drupalGet($entity->toUrl());
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Ensure that the content language cache context is not yet added to the
|
||||
// page.
|
||||
$this->assertCacheContexts($this->defaultCacheContexts);
|
||||
|
||||
$this->drupalGet($entity->toUrl('drupal:content-translation-overview'));
|
||||
$this->assertSession()->pageTextNotContains('Source language');
|
||||
|
||||
$translation = $this->getTranslation($entity, $default_langcode);
|
||||
foreach ($values[$default_langcode] as $property => $value) {
|
||||
$stored_value = $this->getValue($translation, $property, $default_langcode);
|
||||
$value = is_array($value) ? $value[0]['value'] : $value;
|
||||
$message = "$property correctly stored in the default language.";
|
||||
$this->assertEquals($value, $stored_value, $message);
|
||||
}
|
||||
|
||||
// Add a content translation.
|
||||
$langcode = 'it';
|
||||
$language = ConfigurableLanguage::load($langcode);
|
||||
$values[$langcode] = $this->getNewEntityValues($langcode);
|
||||
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
|
||||
$entity->getEntityTypeId() => $entity->id(),
|
||||
'source' => $default_langcode,
|
||||
'target' => $langcode,
|
||||
], ['language' => $language]);
|
||||
$this->drupalGet($add_url);
|
||||
$this->submitForm($this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode));
|
||||
|
||||
// Assert that HTML is not escaped unexpectedly.
|
||||
if ($this->testHTMLEscapeForAllLanguages) {
|
||||
$this->assertSession()->responseNotContains('<span class="translation-entity-all-languages">(all languages)</span>');
|
||||
$this->assertSession()->responseContains('<span class="translation-entity-all-languages">(all languages)</span>');
|
||||
}
|
||||
|
||||
// Ensure that the content language cache context is not yet added to the
|
||||
// page.
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->drupalGet($entity->toUrl());
|
||||
$this->assertCacheContexts(Cache::mergeContexts(['languages:language_content'], $this->defaultCacheContexts));
|
||||
|
||||
// Reset the cache of the entity, so that the new translation gets the
|
||||
// updated values.
|
||||
$metadata_source_translation = $this->manager->getTranslationMetadata($entity->getTranslation($default_langcode));
|
||||
$metadata_target_translation = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
|
||||
|
||||
$author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid';
|
||||
if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) {
|
||||
$this->assertEquals($this->translator->id(), $metadata_target_translation->getAuthor()->id(), "Author of the target translation $langcode correctly stored for translatable owner field.");
|
||||
|
||||
$this->assertNotEquals($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(),
|
||||
"Author of the target translation $langcode different from the author of the source translation $default_langcode for translatable owner field.");
|
||||
}
|
||||
else {
|
||||
$this->assertEquals($this->editor->id(), $metadata_target_translation->getAuthor()->id(), 'Author of the entity remained untouched after translation for non translatable owner field.');
|
||||
}
|
||||
|
||||
$created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created';
|
||||
if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) {
|
||||
// Verify that the translation creation timestamp of the target
|
||||
// translation language is newer than the creation timestamp of the source
|
||||
// translation default language for the translatable created field.
|
||||
$this->assertGreaterThan($metadata_source_translation->getCreatedTime(), $metadata_target_translation->getCreatedTime());
|
||||
}
|
||||
else {
|
||||
$this->assertEquals($metadata_source_translation->getCreatedTime(), $metadata_target_translation->getCreatedTime(), 'Creation timestamp of the entity remained untouched after translation for non translatable created field.');
|
||||
}
|
||||
|
||||
if ($this->testLanguageSelector) {
|
||||
// Verify that language selector is correctly disabled on translations.
|
||||
$this->assertSession()->fieldNotExists('edit-langcode-0-value');
|
||||
}
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->drupalGet($entity->toUrl('drupal:content-translation-overview'));
|
||||
$this->assertSession()->pageTextNotContains('Source language');
|
||||
|
||||
// Switch the source language.
|
||||
$langcode = 'fr';
|
||||
$language = ConfigurableLanguage::load($langcode);
|
||||
$source_langcode = 'it';
|
||||
$edit = ['source_langcode[source]' => $source_langcode];
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
|
||||
$entity->getEntityTypeId() => $entity->id(),
|
||||
'source' => $default_langcode,
|
||||
'target' => $langcode,
|
||||
], ['language' => $language]);
|
||||
// This does not save anything, it merely reloads the form and fills in the
|
||||
// fields with the values from the different source language.
|
||||
$this->drupalGet($add_url);
|
||||
$this->submitForm($edit, 'Change');
|
||||
$this->assertSession()->fieldValueEquals("{$this->fieldName}[0][value]", $values[$source_langcode][$this->fieldName][0]['value']);
|
||||
|
||||
// Add another translation and mark the other ones as outdated.
|
||||
$values[$langcode] = $this->getNewEntityValues($langcode);
|
||||
$edit = $this->getEditValues($values, $langcode) + ['content_translation[retranslate]' => TRUE];
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
|
||||
$entity->getEntityTypeId() => $entity->id(),
|
||||
'source' => $source_langcode,
|
||||
'target' => $langcode,
|
||||
], ['language' => $language]);
|
||||
$this->drupalGet($add_url);
|
||||
$this->submitForm($edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode));
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->drupalGet($entity->toUrl('drupal:content-translation-overview'));
|
||||
$this->assertSession()->pageTextContains('Source language');
|
||||
|
||||
// Check that the entered values have been correctly stored.
|
||||
foreach ($values as $langcode => $property_values) {
|
||||
$translation = $this->getTranslation($entity, $langcode);
|
||||
foreach ($property_values as $property => $value) {
|
||||
$stored_value = $this->getValue($translation, $property, $langcode);
|
||||
$value = is_array($value) ? $value[0]['value'] : $value;
|
||||
$message = "$property correctly stored with language $langcode.";
|
||||
$this->assertEquals($value, $stored_value, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the translation overview shows the correct values.
|
||||
*/
|
||||
protected function doTestTranslationOverview() {
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$translate_url = $entity->toUrl('drupal:content-translation-overview');
|
||||
$this->drupalGet($translate_url);
|
||||
$translate_url->setAbsolute(FALSE);
|
||||
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
if ($entity->hasTranslation($langcode)) {
|
||||
$language = new Language(['id' => $langcode]);
|
||||
// Test that label is correctly shown for translation.
|
||||
$view_url = $entity->toUrl('canonical', ['language' => $language])->toString();
|
||||
$this->assertSession()->elementTextEquals('xpath', "//table//a[@href='{$view_url}']", $entity->getTranslation($langcode)->label() ?? $entity->getTranslation($langcode)->id());
|
||||
// Test that edit link is correct for translation.
|
||||
$edit_path = $entity->toUrl('edit-form', ['language' => $language])->toString();
|
||||
$this->assertSession()->elementTextEquals('xpath', "//table//ul[@class='dropbutton']/li/a[@href='{$edit_path}']", 'Edit');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests up-to-date status tracking.
|
||||
*/
|
||||
protected function doTestOutdatedStatus() {
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$langcode = 'fr';
|
||||
$languages = \Drupal::languageManager()->getLanguages();
|
||||
|
||||
// Mark translations as outdated.
|
||||
$edit = ['content_translation[retranslate]' => TRUE];
|
||||
$edit_path = $entity->toUrl('edit-form', ['language' => $languages[$langcode]]);
|
||||
$this->drupalGet($edit_path);
|
||||
$this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
|
||||
$entity = $storage->load($this->entityId);
|
||||
|
||||
// Check that every translation has the correct "outdated" status, and that
|
||||
// the Translation fieldset is open if the translation is "outdated".
|
||||
foreach ($this->langcodes as $added_langcode) {
|
||||
$url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load($added_langcode)]);
|
||||
$this->drupalGet($url);
|
||||
if ($added_langcode == $langcode) {
|
||||
// Verify that the retranslate flag is not checked by default.
|
||||
$this->assertSession()->fieldValueEquals('content_translation[retranslate]', FALSE);
|
||||
$this->assertSession()->elementNotExists('xpath', '//details[@id="edit-content-translation" and @open="open"]');
|
||||
}
|
||||
else {
|
||||
// Verify that the translate flag is checked by default.
|
||||
$this->assertSession()->fieldValueEquals('content_translation[outdated]', TRUE);
|
||||
$this->assertSession()->elementExists('xpath', '//details[@id="edit-content-translation" and @open="open"]');
|
||||
$edit = ['content_translation[outdated]' => FALSE];
|
||||
$this->drupalGet($url);
|
||||
$this->submitForm($edit, $this->getFormSubmitAction($entity, $added_langcode));
|
||||
$this->drupalGet($url);
|
||||
// Verify that retranslate flag is now shown.
|
||||
$this->assertSession()->fieldValueEquals('content_translation[retranslate]', FALSE);
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$storage->resetCache([$this->entityId]);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($added_langcode))->isOutdated(), 'The "outdated" status has been correctly stored.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the translation publishing status.
|
||||
*/
|
||||
protected function doTestPublishedStatus() {
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
|
||||
// Unpublish translations.
|
||||
foreach ($this->langcodes as $index => $langcode) {
|
||||
if ($index > 0) {
|
||||
$url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load($langcode)]);
|
||||
$edit = ['content_translation[status]' => FALSE];
|
||||
$this->drupalGet($url);
|
||||
$this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$storage->resetCache([$this->entityId]);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($langcode))->isPublished(), 'The translation has been correctly unpublished.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the last published translation cannot be unpublished.
|
||||
$this->drupalGet($entity->toUrl('edit-form'));
|
||||
$this->assertSession()->fieldDisabled('content_translation[status]');
|
||||
$this->assertSession()->fieldValueEquals('content_translation[status]', TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the translation authoring information.
|
||||
*/
|
||||
protected function doTestAuthoringInfo() {
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$values = [];
|
||||
|
||||
// Post different authoring information for each translation.
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
$user = $this->drupalCreateUser();
|
||||
$values[$langcode] = [
|
||||
'uid' => $user->id(),
|
||||
'created' => \Drupal::time()->getRequestTime() - mt_rand(0, 1000),
|
||||
];
|
||||
$edit = [
|
||||
'content_translation[uid]' => $user->getAccountName(),
|
||||
'content_translation[created]' => $this->container->get('date.formatter')->format($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
|
||||
];
|
||||
$url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load($langcode)]);
|
||||
$this->drupalGet($url);
|
||||
$this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
|
||||
}
|
||||
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
$metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
|
||||
$this->assertEquals($values[$langcode]['uid'], $metadata->getAuthor()->id(), 'Translation author correctly stored.');
|
||||
$this->assertEquals($values[$langcode]['created'], $metadata->getCreatedTime(), 'Translation date correctly stored.');
|
||||
}
|
||||
|
||||
// Try to post non valid values and check that they are rejected.
|
||||
$langcode = 'en';
|
||||
$edit = [
|
||||
// User names have by default length 8.
|
||||
'content_translation[uid]' => $this->randomMachineName(12),
|
||||
'content_translation[created]' => '19/11/1978',
|
||||
];
|
||||
$this->drupalGet($entity->toUrl('edit-form'));
|
||||
$this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
|
||||
$this->assertSession()->statusMessageExists('error');
|
||||
$metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
|
||||
$this->assertEquals($values[$langcode]['uid'], $metadata->getAuthor()->id(), 'Translation author correctly kept.');
|
||||
$this->assertEquals($values[$langcode]['created'], $metadata->getCreatedTime(), 'Translation date correctly kept.');
|
||||
|
||||
// Verify that long usernames can be saved as the translation author.
|
||||
$user = $this->drupalCreateUser([], $this->randomMachineName(UserInterface::USERNAME_MAX_LENGTH));
|
||||
$edit = [
|
||||
// Format the username as it is entered in autocomplete fields.
|
||||
'content_translation[uid]' => $user->getAccountName() . ' (' . $user->id() . ')',
|
||||
'content_translation[created]' => $this->container->get('date.formatter')->format($values[$langcode]['created'], 'custom', 'Y-m-d H:i:s O'),
|
||||
];
|
||||
$this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
|
||||
$reloaded_entity = $storage->load($this->entityId);
|
||||
$metadata = $this->manager->getTranslationMetadata($reloaded_entity->getTranslation($langcode));
|
||||
$this->assertEquals($user->id(), $metadata->getAuthor()->id(), 'Translation author correctly set.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests translation deletion.
|
||||
*/
|
||||
protected function doTestTranslationDeletion() {
|
||||
// Confirm and delete a translation.
|
||||
$this->drupalLogin($this->translator);
|
||||
$langcode = 'fr';
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$language = ConfigurableLanguage::load($langcode);
|
||||
$url = $entity->toUrl('edit-form', ['language' => $language]);
|
||||
$this->drupalGet($url);
|
||||
$this->clickLink('Delete translation');
|
||||
$this->submitForm([], 'Delete ' . $language->getName() . ' translation');
|
||||
$entity = $storage->load($this->entityId, TRUE);
|
||||
$this->assertIsObject($entity);
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
$this->assertCount(2, $translations);
|
||||
$this->assertArrayNotHasKey($langcode, $translations);
|
||||
|
||||
// Check that the translator cannot delete the original translation.
|
||||
$args = [$this->entityTypeId => $entity->id(), 'language' => 'en'];
|
||||
$this->drupalGet(Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", $args));
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of entity field values to be tested.
|
||||
*/
|
||||
protected function getNewEntityValues($langcode) {
|
||||
return [$this->fieldName => [['value' => $this->randomMachineName(16)]]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an edit array containing the values to be posted.
|
||||
*/
|
||||
protected function getEditValues($values, $langcode, $new = FALSE) {
|
||||
$edit = $values[$langcode];
|
||||
$langcode = $new ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $langcode;
|
||||
foreach ($values[$langcode] as $property => $value) {
|
||||
if (is_array($value)) {
|
||||
$edit["{$property}[0][value]"] = $value[0]['value'];
|
||||
unset($edit[$property]);
|
||||
}
|
||||
}
|
||||
return $edit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the form action value when submitting a new translation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being tested.
|
||||
* @param string $langcode
|
||||
* Language code for the form.
|
||||
*
|
||||
* @return string
|
||||
* Name of the button to hit.
|
||||
*/
|
||||
protected function getFormSubmitActionForNewTranslation(EntityInterface $entity, $langcode) {
|
||||
$entity->addTranslation($langcode, $entity->toArray());
|
||||
return $this->getFormSubmitAction($entity, $langcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the form action value to be used to submit the entity form.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being tested.
|
||||
* @param string $langcode
|
||||
* Language code for the form.
|
||||
*
|
||||
* @return string
|
||||
* Name of the button to hit.
|
||||
*/
|
||||
protected function getFormSubmitAction(EntityInterface $entity, $langcode) {
|
||||
return 'Save' . $this->getFormSubmitSuffix($entity, $langcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns appropriate submit button suffix based on translatability.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being tested.
|
||||
* @param string $langcode
|
||||
* Language code for the form.
|
||||
*
|
||||
* @return string
|
||||
* Submit button suffix based on translatability.
|
||||
*/
|
||||
protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation object to use to retrieve the translated values.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being tested.
|
||||
* @param string $langcode
|
||||
* The language code identifying the translation to be retrieved.
|
||||
*
|
||||
* @return \Drupal\Core\TypedData\TranslatableInterface
|
||||
* The translation object to act on.
|
||||
*/
|
||||
protected function getTranslation(EntityInterface $entity, $langcode) {
|
||||
return $entity->getTranslation($langcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the specified property in the given language.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $translation
|
||||
* The translation object the property value should be retrieved from.
|
||||
* @param string $property
|
||||
* The property name.
|
||||
* @param string $langcode
|
||||
* The property value.
|
||||
*
|
||||
* @return mixed
|
||||
* The property value.
|
||||
*/
|
||||
protected function getValue(EntityInterface $translation, $property, $langcode) {
|
||||
$key = $property == 'user_id' ? 'target_id' : 'value';
|
||||
return $translation->get($property)->{$key};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the field that implements the changed timestamp.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being tested.
|
||||
*
|
||||
* @return string
|
||||
* The field name.
|
||||
*/
|
||||
protected function getChangedFieldName($entity) {
|
||||
return $entity->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests edit content translation.
|
||||
*/
|
||||
protected function doTestTranslationEdit() {
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$languages = $this->container->get('language_manager')->getLanguages();
|
||||
|
||||
foreach ($this->langcodes as $langcode) {
|
||||
// We only want to test the title for non-english translations.
|
||||
if ($langcode != 'en') {
|
||||
$options = ['language' => $languages[$langcode]];
|
||||
$url = $entity->toUrl('edit-form', $options);
|
||||
$this->drupalGet($url);
|
||||
|
||||
$this->assertSession()->responseContains($entity->getTranslation($langcode)->label());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the basic translation workflow.
|
||||
*/
|
||||
protected function doTestTranslationChanged() {
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$changed_field_name = $this->getChangedFieldName($entity);
|
||||
$definition = $entity->getFieldDefinition($changed_field_name);
|
||||
$config = $definition->getConfig($entity->bundle());
|
||||
|
||||
foreach ([FALSE, TRUE] as $translatable_changed_field) {
|
||||
if ($definition->isTranslatable()) {
|
||||
// For entities defining a translatable changed field we want to test
|
||||
// the correct behavior of that field even if the translatability is
|
||||
// revoked. In that case the changed timestamp should be synchronized
|
||||
// across all translations.
|
||||
$config->setTranslatable($translatable_changed_field);
|
||||
$config->save();
|
||||
}
|
||||
elseif ($translatable_changed_field) {
|
||||
// For entities defining a non-translatable changed field we cannot
|
||||
// declare the field as translatable on the fly by modifying its config
|
||||
// because the schema doesn't support this.
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($entity->getTranslationLanguages() as $language) {
|
||||
// Ensure different timestamps.
|
||||
sleep(1);
|
||||
|
||||
$langcode = $language->getId();
|
||||
|
||||
$edit = [
|
||||
$this->fieldName . '[0][value]' => $this->randomString(),
|
||||
];
|
||||
$edit_path = $entity->toUrl('edit-form', ['language' => $language]);
|
||||
$this->drupalGet($edit_path);
|
||||
$this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
|
||||
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$storage->resetCache([$this->entityId]);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->assertEquals(
|
||||
$entity->getChangedTimeAcrossTranslations(), $entity->getTranslation($langcode)->getChangedTime(),
|
||||
"Changed time for language {$language->getName()} is the latest change over all languages."
|
||||
);
|
||||
}
|
||||
|
||||
$timestamps = [];
|
||||
foreach ($entity->getTranslationLanguages() as $language) {
|
||||
$next_timestamp = $entity->getTranslation($language->getId())->getChangedTime();
|
||||
if (!in_array($next_timestamp, $timestamps)) {
|
||||
$timestamps[] = $next_timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
if ($translatable_changed_field) {
|
||||
$this->assertSameSize($entity->getTranslationLanguages(), $timestamps, 'All timestamps from all languages are different.');
|
||||
}
|
||||
else {
|
||||
$this->assertCount(1, $timestamps, 'All timestamps from all languages are identical.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the changed time after API and FORM save without changes.
|
||||
*/
|
||||
public function doTestChangedTimeAfterSaveWithoutChanges() {
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$entity = $storage->load($this->entityId);
|
||||
// Test only entities, which implement the EntityChangedInterface.
|
||||
if ($entity instanceof EntityChangedInterface) {
|
||||
$changed_timestamp = $entity->getChangedTime();
|
||||
|
||||
$entity->save();
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
$storage->resetCache([$this->entityId]);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->assertEquals($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time wasn\'t updated after API save without changes.');
|
||||
|
||||
// Ensure different save timestamps.
|
||||
sleep(1);
|
||||
|
||||
// Save the entity on the regular edit form.
|
||||
$language = $entity->language();
|
||||
$edit_path = $entity->toUrl('edit-form', ['language' => $language]);
|
||||
$this->drupalGet($edit_path);
|
||||
$this->submitForm([], $this->getFormSubmitAction($entity, $language->getId()));
|
||||
|
||||
$storage->resetCache([$this->entityId]);
|
||||
$entity = $storage->load($this->entityId);
|
||||
$this->assertNotEquals($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time was updated after form save without changes.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\language\Traits\LanguageTestTrait;
|
||||
|
||||
/**
|
||||
* Tests the untranslatable fields behaviors.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationUntranslatableFieldsTest extends ContentTranslationPendingRevisionTestBase {
|
||||
|
||||
use LanguageTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['field_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
|
||||
// Configure one field as untranslatable.
|
||||
$this->drupalLogin($this->administrator);
|
||||
static::setFieldTranslatable($this->entityTypeId, $this->bundle, $this->fieldName, FALSE);
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
|
||||
$entity_field_manager = $this->container->get('entity_field.manager');
|
||||
$entity_field_manager->clearCachedFieldDefinitions();
|
||||
$definitions = $entity_field_manager->getFieldDefinitions($this->entityTypeId, $this->bundle);
|
||||
$this->assertFalse($definitions[$this->fieldName]->isTranslatable());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setupTestFields(): void {
|
||||
parent::setupTestFields();
|
||||
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => 'field_multilingual',
|
||||
'type' => 'test_field',
|
||||
'entity_type' => $this->entityTypeId,
|
||||
'cardinality' => 1,
|
||||
]);
|
||||
$field_storage->save();
|
||||
FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => $this->bundle,
|
||||
'label' => 'Untranslatable-but-visible test field',
|
||||
'translatable' => FALSE,
|
||||
])->save();
|
||||
\Drupal::service('entity_display.repository')->getFormDisplay($this->entityTypeId, $this->bundle, 'default')
|
||||
->setComponent('field_multilingual', [
|
||||
'type' => 'test_field_widget_multilingual',
|
||||
])
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that hiding untranslatable field widgets works correctly.
|
||||
*/
|
||||
public function testHiddenWidgets(): void {
|
||||
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
|
||||
$entity_type_manager = $this->container->get('entity_type.manager');
|
||||
$id = $this->createEntity(['title' => $this->randomString()], 'en');
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $entity_type_manager
|
||||
->getStorage($this->entityTypeId)
|
||||
->load($id);
|
||||
|
||||
// Check that the untranslatable field widget is displayed on the edit form
|
||||
// and no translatability clue is displayed yet.
|
||||
$en_edit_url = $entity->toUrl('edit-form');
|
||||
$this->drupalGet($en_edit_url);
|
||||
$field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
|
||||
$this->assertSession()->elementExists('xpath', $field_xpath);
|
||||
$clue_xpath = '//label[@for="edit-' . strtr($this->fieldName, '_', '-') . '-0-value"]/span[text()="(all languages)"]';
|
||||
$this->assertSession()->elementNotExists('xpath', $clue_xpath);
|
||||
$this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
|
||||
|
||||
// Add a translation and check that the untranslatable field widget is
|
||||
// displayed on the translation and edit forms along with translatability
|
||||
// clues.
|
||||
$add_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
|
||||
$entity->getEntityTypeId() => $entity->id(),
|
||||
'source' => 'en',
|
||||
'target' => 'it',
|
||||
]);
|
||||
$this->drupalGet($add_url);
|
||||
$this->assertSession()->elementExists('xpath', $field_xpath);
|
||||
$this->assertSession()->elementExists('xpath', $clue_xpath);
|
||||
$this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
|
||||
$this->submitForm([], 'Save');
|
||||
|
||||
// Check that the widget is displayed along with its clue in the edit form
|
||||
// for both languages.
|
||||
$this->drupalGet($en_edit_url);
|
||||
$this->assertSession()->elementExists('xpath', $field_xpath);
|
||||
$this->assertSession()->elementExists('xpath', $clue_xpath);
|
||||
$it_edit_url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load('it')]);
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertSession()->elementExists('xpath', $field_xpath);
|
||||
$this->assertSession()->elementExists('xpath', $clue_xpath);
|
||||
|
||||
// Configure untranslatable field widgets to be hidden on non-default
|
||||
// language edit forms.
|
||||
$settings_key = 'settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][content_translation][untranslatable_fields_hide]';
|
||||
$settings_url = 'admin/config/regional/content-language';
|
||||
$this->drupalGet($settings_url);
|
||||
$this->submitForm([$settings_key => 1], 'Save configuration');
|
||||
|
||||
// Verify that the widget is displayed in the default language edit form,
|
||||
// but no clue is displayed.
|
||||
$this->drupalGet($en_edit_url);
|
||||
$field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
|
||||
$this->assertSession()->elementExists('xpath', $field_xpath);
|
||||
$this->assertSession()->elementNotExists('xpath', $clue_xpath);
|
||||
$this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
|
||||
|
||||
// Verify no widget is displayed on the non-default language edit form.
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertSession()->elementNotExists('xpath', $field_xpath);
|
||||
$this->assertSession()->elementNotExists('xpath', $clue_xpath);
|
||||
$this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
|
||||
|
||||
// Verify a warning is displayed.
|
||||
$this->assertSession()->statusMessageContains('Fields that apply to all languages are hidden to avoid conflicting changes.', 'warning');
|
||||
$this->assertSession()->elementExists('xpath', '//a[@href="' . $entity->toUrl('edit-form')->toString() . '" and text()="Edit them on the original language form"]');
|
||||
|
||||
// Configure untranslatable field widgets to be displayed on non-default
|
||||
// language edit forms.
|
||||
$this->drupalGet($settings_url);
|
||||
$this->submitForm([$settings_key => 0], 'Save configuration');
|
||||
|
||||
// Check that the widget is displayed along with its clue in the edit form
|
||||
// for both languages.
|
||||
$this->drupalGet($en_edit_url);
|
||||
$this->assertSession()->elementExists('xpath', $field_xpath);
|
||||
$this->assertSession()->elementExists('xpath', $clue_xpath);
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertSession()->elementExists('xpath', $field_xpath);
|
||||
$this->assertSession()->elementExists('xpath', $clue_xpath);
|
||||
|
||||
// Enable content moderation and verify that widgets are hidden despite them
|
||||
// being configured to be displayed.
|
||||
$this->enableContentModeration();
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertSession()->elementNotExists('xpath', $field_xpath);
|
||||
$this->assertSession()->elementNotExists('xpath', $clue_xpath);
|
||||
|
||||
// Verify a warning is displayed.
|
||||
$this->assertSession()->statusMessageContains('Fields that apply to all languages are hidden to avoid conflicting changes.', 'warning');
|
||||
$this->assertSession()->elementExists('xpath', '//a[@href="' . $entity->toUrl('edit-form')->toString() . '" and text()="Edit them on the original language form"]');
|
||||
|
||||
// Verify that checkboxes on the language content settings page are checked
|
||||
// and disabled for moderated bundles.
|
||||
$this->drupalGet($settings_url);
|
||||
$field_name = "settings[{$this->entityTypeId}][{$this->bundle}][settings][content_translation][untranslatable_fields_hide]";
|
||||
$this->assertSession()->fieldValueEquals($field_name, 1);
|
||||
$this->assertSession()->fieldDisabled($field_name);
|
||||
$this->submitForm([$settings_key => 0], 'Save configuration');
|
||||
$this->assertSession()->fieldValueEquals($field_name, 1);
|
||||
$this->assertSession()->fieldDisabled($field_name);
|
||||
|
||||
// Verify that the untranslatable fields warning message is not displayed
|
||||
// when submitting.
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertSession()->pageTextContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
|
||||
$this->submitForm([], 'Save (this translation)');
|
||||
$this->assertSession()->pageTextNotContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\entity_test\Entity\EntityTestMulRevPub;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
use Drupal\user\UserInterface;
|
||||
|
||||
/**
|
||||
* Tests the content translation workflows for the test entity.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
|
||||
|
||||
use AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* The entity used for testing.
|
||||
*
|
||||
* @var \Drupal\entity_test\Entity\EntityTestMul
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $entityTypeId = 'entity_test_mulrevpub';
|
||||
|
||||
/**
|
||||
* The referencing entity.
|
||||
*
|
||||
* @var \Drupal\entity_test\Entity\EntityTestMul
|
||||
*/
|
||||
protected $referencingEntity;
|
||||
|
||||
/**
|
||||
* The entity owner account to be used to test multilingual entity editing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $entityOwner;
|
||||
|
||||
/**
|
||||
* The user that has entity owner permission but is not the owner.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $notEntityOwner;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'content_translation',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => 'field_reference',
|
||||
'type' => 'entity_reference',
|
||||
'entity_type' => $this->entityTypeId,
|
||||
'cardinality' => 1,
|
||||
'settings' => [
|
||||
'target_type' => $this->entityTypeId,
|
||||
],
|
||||
]);
|
||||
$field_storage->save();
|
||||
FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => $this->entityTypeId,
|
||||
'label' => 'Reference',
|
||||
'translatable' => FALSE,
|
||||
])->save();
|
||||
|
||||
$this->container->get('entity_display.repository')
|
||||
->getViewDisplay($this->entityTypeId, $this->entityTypeId, 'default')
|
||||
->setComponent('field_reference', [
|
||||
'type' => 'entity_reference_entity_view',
|
||||
])
|
||||
->save();
|
||||
|
||||
$this->setupEntity();
|
||||
|
||||
// Create a second entity that references the first to test how the
|
||||
// translation can be viewed through an entity reference field.
|
||||
$this->referencingEntity = EntityTestMulRevPub::create([
|
||||
'name' => 'referencing',
|
||||
'field_reference' => $this->entity->id(),
|
||||
]);
|
||||
$this->referencingEntity->addTranslation($this->langcodes[2], $this->referencingEntity->toArray());
|
||||
$this->referencingEntity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setupUsers(): void {
|
||||
$this->entityOwner = $this->drupalCreateUser($this->getEntityOwnerPermissions(), 'entity_owner');
|
||||
$this->notEntityOwner = $this->drupalCreateUser();
|
||||
$this->notEntityOwner->set('roles', $this->entityOwner->getRoles(TRUE));
|
||||
$this->notEntityOwner->save();
|
||||
parent::setupUsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of permissions needed for the entity owner.
|
||||
*/
|
||||
protected function getEntityOwnerPermissions(): array {
|
||||
return ['edit own entity_test content', 'translate editable entities', 'view test entity', 'view test entity translations', 'view unpublished test entity translations'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getTranslatorPermissions() {
|
||||
$permissions = parent::getTranslatorPermissions();
|
||||
$permissions[] = 'view test entity';
|
||||
$permissions[] = 'view test entity translations';
|
||||
$permissions[] = 'view unpublished test entity translations';
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditorPermissions(): array {
|
||||
return ['administer entity_test content', 'view test entity', 'view test entity translations'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test entity and translate it.
|
||||
*
|
||||
* @param \Drupal\User\UserInterface|null $user
|
||||
* (optional) The entity owner.
|
||||
*/
|
||||
protected function setupEntity(?UserInterface $user = NULL): void {
|
||||
$default_langcode = $this->langcodes[0];
|
||||
|
||||
// Create a test entity.
|
||||
$user = $user ?: $this->drupalCreateUser();
|
||||
$values = [
|
||||
'name' => $this->randomMachineName(),
|
||||
'user_id' => $user->id(),
|
||||
$this->fieldName => [['value' => $this->randomMachineName(16)]],
|
||||
];
|
||||
$id = $this->createEntity($values, $default_langcode);
|
||||
$storage = $this->container->get('entity_type.manager')
|
||||
->getStorage($this->entityTypeId);
|
||||
|
||||
// Create a translation that is not published to test view access.
|
||||
$this->drupalLogin($this->translator);
|
||||
$add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $id, 'source' => $default_langcode, 'target' => $this->langcodes[2]]);
|
||||
$edit = [
|
||||
'name[0][value]' => 'translation name',
|
||||
'content_translation[status]' => FALSE,
|
||||
];
|
||||
$this->drupalGet($add_translation_url);
|
||||
$this->submitForm($edit, 'Save');
|
||||
|
||||
$storage->resetCache([$id]);
|
||||
$this->entity = $storage->load($id);
|
||||
|
||||
$this->rebuildContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests simple and editorial translation workflows.
|
||||
*/
|
||||
public function testWorkflows(): void {
|
||||
// Test workflows for the editor.
|
||||
$expected_status = [
|
||||
'edit' => 200,
|
||||
'delete' => 200,
|
||||
'overview' => 403,
|
||||
'add_translation' => 403,
|
||||
'edit_translation' => 403,
|
||||
'delete_translation' => 403,
|
||||
'view_unpublished_translation' => 403,
|
||||
'view_unpublished_translation_reference' => FALSE,
|
||||
];
|
||||
$this->doTestWorkflows($this->editor, $expected_status);
|
||||
|
||||
// Test workflows for the translator.
|
||||
$expected_status = [
|
||||
'edit' => 403,
|
||||
'delete' => 403,
|
||||
'overview' => 200,
|
||||
'add_translation' => 200,
|
||||
'edit_translation' => 200,
|
||||
'delete_translation' => 200,
|
||||
'view_unpublished_translation' => 200,
|
||||
'view_unpublished_translation_reference' => TRUE,
|
||||
];
|
||||
$this->doTestWorkflows($this->translator, $expected_status);
|
||||
|
||||
// Test workflows for the admin.
|
||||
$expected_status = [
|
||||
'edit' => 200,
|
||||
'delete' => 200,
|
||||
'overview' => 200,
|
||||
'add_translation' => 200,
|
||||
'edit_translation' => 403,
|
||||
'delete_translation' => 403,
|
||||
'view_unpublished_translation' => 200,
|
||||
'view_unpublished_translation_reference' => TRUE,
|
||||
];
|
||||
$this->doTestWorkflows($this->administrator, $expected_status);
|
||||
|
||||
// Check that translation permissions allow the associated operations.
|
||||
$ops = ['create' => 'Add', 'update' => 'Edit', 'delete' => 'Delete'];
|
||||
$translations_url = $this->entity->toUrl('drupal:content-translation-overview');
|
||||
foreach ($ops as $current_op => $item) {
|
||||
$user = $this->drupalCreateUser([
|
||||
$this->getTranslatePermission(),
|
||||
"$current_op content translations",
|
||||
'view test entity',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
$this->drupalGet($translations_url);
|
||||
|
||||
// Make sure that the user.permissions cache context and the cache tags
|
||||
// for the entity are present.
|
||||
$this->assertCacheContext('user.permissions');
|
||||
foreach ($this->entity->getCacheTags() as $cache_tag) {
|
||||
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', $cache_tag);
|
||||
}
|
||||
|
||||
foreach ($ops as $op => $label) {
|
||||
if ($op != $current_op) {
|
||||
$this->assertSession()->linkNotExists($label);
|
||||
}
|
||||
else {
|
||||
$this->assertSession()->linkExists($label, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test workflows for the entity owner with non-editable content.
|
||||
$expected_status = [
|
||||
'edit' => 403,
|
||||
'delete' => 403,
|
||||
'overview' => 403,
|
||||
'add_translation' => 403,
|
||||
'edit_translation' => 403,
|
||||
'delete_translation' => 403,
|
||||
'view_unpublished_translation' => 200,
|
||||
'view_unpublished_translation_reference' => TRUE,
|
||||
];
|
||||
$this->doTestWorkflows($this->entityOwner, $expected_status);
|
||||
|
||||
// Test workflows for the entity owner with editable content.
|
||||
$this->setupEntity($this->entityOwner);
|
||||
$this->referencingEntity->set('field_reference', $this->entity->id());
|
||||
$this->referencingEntity->save();
|
||||
$expected_status = [
|
||||
'edit' => 200,
|
||||
'delete' => 403,
|
||||
'overview' => 200,
|
||||
'add_translation' => 200,
|
||||
'edit_translation' => 200,
|
||||
'delete_translation' => 200,
|
||||
'view_unpublished_translation' => 200,
|
||||
'view_unpublished_translation_reference' => TRUE,
|
||||
];
|
||||
$this->doTestWorkflows($this->entityOwner, $expected_status);
|
||||
$expected_status = [
|
||||
'edit' => 403,
|
||||
'delete' => 403,
|
||||
'overview' => 403,
|
||||
'add_translation' => 403,
|
||||
'edit_translation' => 403,
|
||||
'delete_translation' => 403,
|
||||
'view_unpublished_translation' => 200,
|
||||
'view_unpublished_translation_reference' => TRUE,
|
||||
];
|
||||
$this->doTestWorkflows($this->notEntityOwner, $expected_status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that workflows have the expected behaviors for the given user.
|
||||
*
|
||||
* @param \Drupal\user\UserInterface $user
|
||||
* The user to test the workflow behavior against.
|
||||
* @param array $expected_status
|
||||
* The an associative array with the operation name as key and the expected
|
||||
* status as value.
|
||||
*/
|
||||
protected function doTestWorkflows(UserInterface $user, $expected_status): void {
|
||||
$default_langcode = $this->langcodes[0];
|
||||
$languages = $this->container->get('language_manager')->getLanguages();
|
||||
$options = ['language' => $languages[$default_langcode], 'absolute' => TRUE];
|
||||
$this->drupalLogin($user);
|
||||
|
||||
// Check whether the user is allowed to access the entity form in edit mode.
|
||||
$edit_url = $this->entity->toUrl('edit-form', $options);
|
||||
$this->drupalGet($edit_url, $options);
|
||||
$this->assertSession()->statusCodeEquals($expected_status['edit']);
|
||||
|
||||
// Check whether the user is allowed to access the entity delete form.
|
||||
$delete_url = $this->entity->toUrl('delete-form', $options);
|
||||
$this->drupalGet($delete_url, $options);
|
||||
$this->assertSession()->statusCodeEquals($expected_status['delete']);
|
||||
|
||||
// Check whether the user is allowed to access the translation overview.
|
||||
$langcode = $this->langcodes[1];
|
||||
$options['language'] = $languages[$langcode];
|
||||
$translations_url = $this->entity->toUrl('drupal:content-translation-overview', $options)->toString();
|
||||
$this->drupalGet($translations_url);
|
||||
$this->assertSession()->statusCodeEquals($expected_status['overview']);
|
||||
|
||||
// Check whether the user is allowed to create a translation.
|
||||
$add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
|
||||
if ($expected_status['add_translation'] == 200) {
|
||||
$this->clickLink('Add');
|
||||
$this->assertSession()->addressEquals($add_translation_url);
|
||||
// Check that the translation form does not contain shared elements for
|
||||
// translators.
|
||||
if ($expected_status['edit'] == 403) {
|
||||
$this->assertNoSharedElements();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->drupalGet($add_translation_url);
|
||||
}
|
||||
$this->assertSession()->statusCodeEquals($expected_status['add_translation']);
|
||||
|
||||
// Check whether the user is allowed to edit a translation.
|
||||
$langcode = $this->langcodes[2];
|
||||
$options['language'] = $languages[$langcode];
|
||||
$edit_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_edit", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
|
||||
if ($expected_status['edit_translation'] == 200) {
|
||||
$this->drupalGet($translations_url);
|
||||
$editor = $expected_status['edit'] == 200;
|
||||
|
||||
if ($editor) {
|
||||
$this->clickLink('Edit', 1);
|
||||
// An editor should be pointed to the entity form in multilingual mode.
|
||||
// We need a new expected edit path with a new language.
|
||||
$expected_edit_path = $this->entity->toUrl('edit-form', $options)->toString();
|
||||
$this->assertSession()->addressEquals($expected_edit_path);
|
||||
}
|
||||
else {
|
||||
$this->clickLink('Edit');
|
||||
// While a translator should be pointed to the translation form.
|
||||
$this->assertSession()->addressEquals($edit_translation_url);
|
||||
// Check that the translation form does not contain shared elements.
|
||||
$this->assertNoSharedElements();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->drupalGet($edit_translation_url);
|
||||
}
|
||||
$this->assertSession()->statusCodeEquals($expected_status['edit_translation']);
|
||||
|
||||
// When viewing an unpublished entity directly, access is currently denied
|
||||
// completely. See https://www.drupal.org/node/2978048.
|
||||
$this->drupalGet($this->entity->getTranslation($langcode)->toUrl());
|
||||
$this->assertSession()->statusCodeEquals($expected_status['view_unpublished_translation']);
|
||||
|
||||
// On a reference field, the translation falls back to the default language.
|
||||
$this->drupalGet($this->referencingEntity->getTranslation($langcode)->toUrl());
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
if ($expected_status['view_unpublished_translation_reference']) {
|
||||
$this->assertSession()->pageTextContains('translation name');
|
||||
}
|
||||
else {
|
||||
$this->assertSession()->pageTextContains($this->entity->label());
|
||||
}
|
||||
|
||||
// Check whether the user is allowed to delete a translation.
|
||||
$delete_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_delete", [$this->entityTypeId => $this->entity->id(), 'language' => $langcode], $options);
|
||||
if ($expected_status['delete_translation'] == 200) {
|
||||
$this->drupalGet($translations_url);
|
||||
$editor = $expected_status['delete'] == 200;
|
||||
|
||||
if ($editor) {
|
||||
$this->clickLink('Delete', 1);
|
||||
// An editor should be pointed to the entity deletion form in
|
||||
// multilingual mode. We need a new expected delete path with a new
|
||||
// language.
|
||||
$expected_delete_path = $this->entity->toUrl('delete-form', $options)->toString();
|
||||
$this->assertSession()->addressEquals($expected_delete_path);
|
||||
}
|
||||
else {
|
||||
$this->clickLink('Delete');
|
||||
// While a translator should be pointed to the translation deletion
|
||||
// form.
|
||||
$this->assertSession()->addressEquals($delete_translation_url);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->drupalGet($delete_translation_url);
|
||||
}
|
||||
$this->assertSession()->statusCodeEquals($expected_status['delete_translation']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the current page does not contain shared form elements.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertNoSharedElements(): void {
|
||||
$language_none = LanguageInterface::LANGCODE_NOT_SPECIFIED;
|
||||
$this->assertSession()->fieldNotExists("field_test_text[$language_none][0][value]");
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
|
||||
|
||||
/**
|
||||
* Generic module test for content_translation.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class GenericTest extends GenericModuleTestBase {}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional\Views;
|
||||
|
||||
use Drupal\Tests\views_ui\Functional\UITestBase;
|
||||
|
||||
/**
|
||||
* Tests the views UI when content_translation is enabled.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationViewsUITest extends UITestBase {
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = ['test_view'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['content_translation'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Tests the views UI.
|
||||
*/
|
||||
public function testViewsUI(): void {
|
||||
$this->drupalGet('admin/structure/views/view/test_view/edit');
|
||||
$this->assertSession()->titleEquals('Test view (Views test data) | Drupal');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional\Views;
|
||||
|
||||
use Drupal\Tests\content_translation\Functional\ContentTranslationTestBase;
|
||||
use Drupal\views\Tests\ViewTestData;
|
||||
use Drupal\Core\Language\Language;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* Tests the content translation overview link field handler.
|
||||
*
|
||||
* @group content_translation
|
||||
* @see \Drupal\content_translation\Plugin\views\field\TranslationLink
|
||||
*/
|
||||
class TranslationLinkTest extends ContentTranslationTestBase {
|
||||
|
||||
/**
|
||||
* Views used by this test.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $testViews = ['test_entity_translations_link'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['content_translation_test_views'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
// @todo Use entity_type once it has multilingual Views integration.
|
||||
$this->entityTypeId = 'user';
|
||||
|
||||
parent::setUp();
|
||||
$this->doSetup();
|
||||
|
||||
// Assign user 1 a language code so that the entity can be translated.
|
||||
$user = User::load(1);
|
||||
$user->langcode = 'en';
|
||||
$user->save();
|
||||
|
||||
// Assign user 2 LANGCODE_NOT_SPECIFIED code so entity can't be translated.
|
||||
$user = User::load(2);
|
||||
$user->langcode = Language::LANGCODE_NOT_SPECIFIED;
|
||||
$user->save();
|
||||
|
||||
ViewTestData::createTestViews(static::class, ['content_translation_test_views']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getTranslatorPermissions() {
|
||||
$permissions = parent::getTranslatorPermissions();
|
||||
$permissions[] = 'access user profiles';
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the content translation overview link field handler.
|
||||
*/
|
||||
public function testTranslationLink(): void {
|
||||
$this->drupalGet('test-entity-translations-link');
|
||||
$this->assertSession()->linkByHrefExists('user/1/translations');
|
||||
$this->assertSession()->linkByHrefNotExists('user/2/translations', 'The translations link is not present when content_translation_translate_access() is FALSE.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\FunctionalJavascript;
|
||||
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
|
||||
/**
|
||||
* Tests that the content translation configuration javascript does't fail.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationConfigUITest extends WebDriverTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['content_translation', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Use the minimal profile.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $profile = 'standard';
|
||||
|
||||
/**
|
||||
* Tests that the content translation configuration javascript does't fail.
|
||||
*/
|
||||
public function testContentTranslationConfigUI(): void {
|
||||
$content_translation_manager = $this->container->get('content_translation.manager');
|
||||
$content_translation_manager->setEnabled('node', 'article', TRUE);
|
||||
$this->rebuildContainer();
|
||||
|
||||
$admin = $this->drupalCreateUser([], NULL, TRUE);
|
||||
$this->drupalLogin($admin);
|
||||
$this->drupalGet('/admin/config/regional/content-language');
|
||||
$this->failOnJavaScriptErrors();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\FunctionalJavascript;
|
||||
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests that contextual links are available for content translation.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationContextualLinksTest extends WebDriverTestBase {
|
||||
|
||||
/**
|
||||
* The 'translator' user to use during testing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['content_translation', 'contextual', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Set up an additional language.
|
||||
ConfigurableLanguage::createFromLangcode('es')->save();
|
||||
|
||||
// Create a content type.
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
|
||||
// Enable content translation.
|
||||
$content_translation_manager = $this->container->get('content_translation.manager');
|
||||
$content_translation_manager->setEnabled('node', 'page', TRUE);
|
||||
$this->rebuildContainer();
|
||||
|
||||
// Create a translator user.
|
||||
$permissions = [
|
||||
'access contextual links',
|
||||
'administer nodes',
|
||||
'edit any page content',
|
||||
'translate any entity',
|
||||
];
|
||||
$this->translator = $this->drupalCreateUser($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a contextual link is available for translating a node.
|
||||
*/
|
||||
public function testContentTranslationContextualLinks(): void {
|
||||
$node = $this->drupalCreateNode(['type' => 'page', 'title' => 'Test']);
|
||||
|
||||
// Check that the translate link appears on the node page.
|
||||
$this->drupalLogin($this->translator);
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
$link = $this->assertSession()->waitForElement('css', '[data-contextual-id^="node:node=1"] .contextual-links a:contains("Translate")');
|
||||
$this->assertStringContainsString('node/1/translations', $link->getAttribute('href'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Kernel;
|
||||
|
||||
use Drupal\Core\Config\ConfigImporter;
|
||||
use Drupal\Core\Config\StorageComparer;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests content translation updates performed during config import.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationConfigImportTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Config Importer object used for testing.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigImporter
|
||||
*/
|
||||
protected $configImporter;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'system',
|
||||
'user',
|
||||
'entity_test',
|
||||
'language',
|
||||
'content_translation',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->installConfig(['system']);
|
||||
$this->installEntitySchema('entity_test_mul');
|
||||
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
|
||||
|
||||
// Set up the ConfigImporter object for testing.
|
||||
$storage_comparer = new StorageComparer(
|
||||
$this->container->get('config.storage.sync'),
|
||||
$this->container->get('config.storage')
|
||||
);
|
||||
$this->configImporter = new ConfigImporter(
|
||||
$storage_comparer->createChangelist(),
|
||||
$this->container->get('event_dispatcher'),
|
||||
$this->container->get('config.manager'),
|
||||
$this->container->get('lock'),
|
||||
$this->container->get('config.typed'),
|
||||
$this->container->get('module_handler'),
|
||||
$this->container->get('module_installer'),
|
||||
$this->container->get('theme_handler'),
|
||||
$this->container->get('string_translation'),
|
||||
$this->container->get('extension.list.module'),
|
||||
$this->container->get('extension.list.theme')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests config import updates.
|
||||
*/
|
||||
public function testConfigImportUpdates(): void {
|
||||
$entity_type_id = 'entity_test_mul';
|
||||
$config_id = $entity_type_id . '.' . $entity_type_id;
|
||||
$config_name = 'language.content_settings.' . $config_id;
|
||||
$storage = $this->container->get('config.storage');
|
||||
$sync = $this->container->get('config.storage.sync');
|
||||
|
||||
// Verify the configuration to create does not exist yet.
|
||||
$this->assertFalse($storage->exists($config_name), $config_name . ' not found.');
|
||||
|
||||
// Create new config entity.
|
||||
$data = [
|
||||
'uuid' => 'a019d89b-c4d9-4ed4-b859-894e4e2e93cf',
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'dependencies' => [
|
||||
'module' => ['content_translation'],
|
||||
],
|
||||
'id' => $config_id,
|
||||
'target_entity_type_id' => 'entity_test_mul',
|
||||
'target_bundle' => 'entity_test_mul',
|
||||
'default_langcode' => 'site_default',
|
||||
'language_alterable' => FALSE,
|
||||
'third_party_settings' => [
|
||||
'content_translation' => ['enabled' => TRUE],
|
||||
],
|
||||
];
|
||||
$sync->write($config_name, $data);
|
||||
$this->assertTrue($sync->exists($config_name), $config_name . ' found.');
|
||||
|
||||
// Import.
|
||||
$this->configImporter->reset()->import();
|
||||
|
||||
// Verify the values appeared.
|
||||
$config = $this->config($config_name);
|
||||
$this->assertSame($config_id, $config->get('id'));
|
||||
|
||||
// Verify that updates were performed.
|
||||
$entity_type = $this->container->get('entity_type.manager')->getDefinition($entity_type_id);
|
||||
$table = $entity_type->getDataTable();
|
||||
$db_schema = $this->container->get('database')->schema();
|
||||
$result = $db_schema->fieldExists($table, 'content_translation_source') && $db_schema->fieldExists($table, 'content_translation_outdated');
|
||||
$this->assertTrue($result, 'Content translation updates were successfully performed during config import.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\entity_test\Entity\EntityTestMul;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* Tests the Content Translation bundle info logic.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationEntityBundleInfoTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'system',
|
||||
'node',
|
||||
'user',
|
||||
'language',
|
||||
'content_translation_test',
|
||||
'content_translation',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* The bundle info service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfo
|
||||
*/
|
||||
protected $bundleInfo;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->contentTranslationManager = $this->container->get('content_translation.manager');
|
||||
$this->bundleInfo = $this->container->get('entity_type.bundle.info');
|
||||
|
||||
$this->installEntitySchema('entity_test_mul');
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('it')->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that modules can know whether bundles are translatable.
|
||||
*/
|
||||
public function testHookInvocationOrder(): void {
|
||||
$this->contentTranslationManager->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
|
||||
$this->bundleInfo->clearCachedBundles();
|
||||
$this->bundleInfo->getAllBundleInfo();
|
||||
|
||||
// Verify that the test module comes first in the module list, which would
|
||||
// normally make its hook implementation to be invoked first.
|
||||
/** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
|
||||
$module_handler = $this->container->get('module_handler');
|
||||
$module_list = $module_handler->getModuleList();
|
||||
$expected_modules = [
|
||||
'content_translation_test',
|
||||
'content_translation',
|
||||
];
|
||||
$actual_modules = array_keys(array_intersect_key($module_list, array_flip($expected_modules)));
|
||||
$this->assertEquals($expected_modules, $actual_modules);
|
||||
|
||||
// Check that the "content_translation_test" hook implementation has access
|
||||
// to the "translatable" bundle info property.
|
||||
/** @var \Drupal\Core\State\StateInterface $state */
|
||||
$state = $this->container->get('state');
|
||||
$this->assertTrue($state->get('content_translation_test.translatable'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that field synchronization is skipped for disabled bundles.
|
||||
*/
|
||||
public function testFieldSynchronizationWithDisabledBundle(): void {
|
||||
$entity = EntityTestMul::create();
|
||||
$entity->save();
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $translation */
|
||||
$translation = $entity->addTranslation('it');
|
||||
$translation->save();
|
||||
|
||||
$this->assertTrue($entity->isTranslatable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that bundle translation settings are propagated on creation.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
*/
|
||||
public function testBundleClearOnLanguageContentSettingInsert(): void {
|
||||
$node = $this->getBundledNode();
|
||||
$this->assertFalse($node->isTranslatable());
|
||||
$this->contentTranslationManager->setEnabled('node', 'bundle_test', TRUE);
|
||||
$this->assertTrue($node->isTranslatable(), "Bundle info was not cleared on language_content_settings creation.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that bundle translation setting changes are propagated.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testBundleClearOnLanguageContentSettingUpdate(): void {
|
||||
$node = $this->getBundledNode();
|
||||
$this->assertFalse($node->isTranslatable());
|
||||
$this->container->get('entity_type.manager')->getStorage('language_content_settings')->create([
|
||||
'target_entity_type_id' => 'node',
|
||||
'target_bundle' => 'bundle_test',
|
||||
])->save();
|
||||
$this->assertFalse($node->isTranslatable());
|
||||
$this->contentTranslationManager->setEnabled('node', 'bundle_test', TRUE);
|
||||
$this->assertTrue($node->isTranslatable(), "Bundle info was not cleared on language_content_settings update.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new bundled node for testing.
|
||||
*
|
||||
* @return \Drupal\node\Entity\Node
|
||||
* The new node.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
*/
|
||||
protected function getBundledNode() {
|
||||
$this->installEntitySchema('node');
|
||||
$bundle = NodeType::create([
|
||||
'type' => 'bundle_test',
|
||||
'name' => 'Bundle Test',
|
||||
]);
|
||||
$bundle->save();
|
||||
$node = Node::create([
|
||||
'type' => 'bundle_test',
|
||||
]);
|
||||
return $node;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,581 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Kernel;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\entity_test\Entity\EntityTestMulRev;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* Tests the field synchronization logic when revisions are involved.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationFieldSyncRevisionTest extends EntityKernelTestBase {
|
||||
|
||||
use TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'file',
|
||||
'image',
|
||||
'language',
|
||||
'content_translation',
|
||||
'content_translation_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* The synchronized field name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldName = 'sync_field';
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface|\Drupal\content_translation\BundleTranslationSettingsInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* The test entity storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$entity_type_id = 'entity_test_mulrev';
|
||||
$this->installEntitySchema($entity_type_id);
|
||||
$this->installEntitySchema('file');
|
||||
$this->installSchema('file', ['file_usage']);
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('it')->save();
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
|
||||
/** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
|
||||
$field_storage_config = FieldStorageConfig::create([
|
||||
'field_name' => $this->fieldName,
|
||||
'type' => 'image',
|
||||
'entity_type' => $entity_type_id,
|
||||
'cardinality' => 1,
|
||||
'translatable' => 1,
|
||||
]);
|
||||
$field_storage_config->save();
|
||||
|
||||
$field_config = FieldConfig::create([
|
||||
'entity_type' => $entity_type_id,
|
||||
'field_name' => $this->fieldName,
|
||||
'bundle' => $entity_type_id,
|
||||
'label' => 'Synchronized field',
|
||||
'translatable' => 1,
|
||||
]);
|
||||
$field_config->save();
|
||||
|
||||
$property_settings = [
|
||||
'alt' => 'alt',
|
||||
'title' => 'title',
|
||||
'file' => 0,
|
||||
];
|
||||
$field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings);
|
||||
$field_config->save();
|
||||
|
||||
$this->contentTranslationManager = $this->container->get('content_translation.manager');
|
||||
$this->contentTranslationManager->setEnabled($entity_type_id, $entity_type_id, TRUE);
|
||||
|
||||
$this->storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
|
||||
foreach ($this->getTestFiles('image') as $file) {
|
||||
$entity = File::create((array) $file + ['status' => 1]);
|
||||
$entity->save();
|
||||
}
|
||||
|
||||
$this->state->set('content_translation.entity_access.file', ['view' => TRUE]);
|
||||
|
||||
$account = User::create([
|
||||
'name' => $this->randomMachineName(),
|
||||
'status' => 1,
|
||||
]);
|
||||
$account->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that field synchronization works as expected with revisions.
|
||||
*
|
||||
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::create
|
||||
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::validate
|
||||
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::hasSynchronizedPropertyChanges
|
||||
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::getFieldSynchronizedProperties
|
||||
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeFields
|
||||
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeItems
|
||||
*/
|
||||
public function testFieldSynchronizationAndValidation(): void {
|
||||
// Test that when untranslatable field widgets are displayed, synchronized
|
||||
// field properties can be changed only in default revisions.
|
||||
$this->setUntranslatableFieldWidgetsDisplay(TRUE);
|
||||
$entity = $this->saveNewEntity();
|
||||
$entity_id = $entity->id();
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [1, 1, 1, 'Alt 1 EN']);
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
|
||||
$en_revision = $this->createRevision($entity, FALSE);
|
||||
$en_revision->get($this->fieldName)->target_id = 2;
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertViolations($violations);
|
||||
|
||||
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||
$it_revision = $this->createRevision($it_translation, FALSE);
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||
$metadata->setSource('en');
|
||||
$it_revision->get($this->fieldName)->target_id = 2;
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertViolations($violations);
|
||||
$it_revision->isDefaultRevision(TRUE);
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [2, 2, 2, 'Alt 1 EN', 'Alt 2 IT']);
|
||||
|
||||
$en_revision = $this->createRevision($en_revision, FALSE);
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [3, 2, 2, 'Alt 3 EN', 'Alt 2 IT']);
|
||||
|
||||
$it_revision = $this->createRevision($it_revision, FALSE);
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [4, 2, 2, 'Alt 1 EN', 'Alt 4 IT']);
|
||||
|
||||
$en_revision = $this->createRevision($en_revision);
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 5 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [5, 2, 2, 'Alt 5 EN', 'Alt 2 IT']);
|
||||
|
||||
$en_revision = $this->createRevision($en_revision);
|
||||
$en_revision->get($this->fieldName)->target_id = 6;
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [6, 6, 6, 'Alt 6 EN', 'Alt 2 IT']);
|
||||
|
||||
$it_revision = $this->createRevision($it_revision);
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [7, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
|
||||
|
||||
// Test that when untranslatable field widgets are hidden, synchronized
|
||||
// field properties can be changed only when editing the default
|
||||
// translation. This may lead to temporarily desynchronized values, when
|
||||
// saving a pending revision for the default translation that changes a
|
||||
// synchronized property (see revision 11).
|
||||
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
|
||||
$entity = $this->saveNewEntity();
|
||||
$entity_id = $entity->id();
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [8, 1, 1, 'Alt 1 EN']);
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
|
||||
$en_revision = $this->createRevision($entity, FALSE);
|
||||
$en_revision->get($this->fieldName)->target_id = 2;
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 2 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [9, 2, 2, 'Alt 2 EN']);
|
||||
|
||||
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||
$it_revision = $this->createRevision($it_translation, FALSE);
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||
$metadata->setSource('en');
|
||||
$it_revision->get($this->fieldName)->target_id = 3;
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertViolations($violations);
|
||||
$it_revision->isDefaultRevision(TRUE);
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertViolations($violations);
|
||||
|
||||
$it_revision = $this->createRevision($it_translation);
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||
$metadata->setSource('en');
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 3 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [10, 1, 1, 'Alt 1 EN', 'Alt 3 IT']);
|
||||
|
||||
$en_revision = $this->createRevision($en_revision, FALSE);
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [11, 2, 1, 'Alt 4 EN', 'Alt 3 IT']);
|
||||
|
||||
$it_revision = $this->createRevision($it_revision, FALSE);
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 5 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [12, 1, 1, 'Alt 1 EN', 'Alt 5 IT']);
|
||||
|
||||
$en_revision = $this->createRevision($en_revision);
|
||||
$en_revision->get($this->fieldName)->target_id = 6;
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [13, 6, 6, 'Alt 6 EN', 'Alt 3 IT']);
|
||||
|
||||
$it_revision = $this->createRevision($it_revision);
|
||||
$it_revision->get($this->fieldName)->target_id = 7;
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertViolations($violations);
|
||||
|
||||
$it_revision = $this->createRevision($it_revision);
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [14, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
|
||||
|
||||
// Test that creating a default revision starting from a pending revision
|
||||
// having changes to synchronized properties, without introducing new
|
||||
// changes works properly.
|
||||
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
|
||||
$entity = $this->saveNewEntity();
|
||||
$entity_id = $entity->id();
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [15, 1, 1, 'Alt 1 EN']);
|
||||
|
||||
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||
$it_revision = $this->createRevision($it_translation);
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||
$metadata->setSource('en');
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [16, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
|
||||
$en_revision = $this->createRevision($entity);
|
||||
$en_revision->get($this->fieldName)->target_id = 3;
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [17, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
|
||||
|
||||
$en_revision = $this->createRevision($entity, FALSE);
|
||||
$en_revision->get($this->fieldName)->target_id = 4;
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [18, 4, 3, 'Alt 4 EN', 'Alt 2 IT']);
|
||||
|
||||
$en_revision = $this->createRevision($entity);
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [19, 4, 4, 'Alt 4 EN', 'Alt 2 IT']);
|
||||
|
||||
$it_revision = $this->createRevision($it_revision);
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 6 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [20, 4, 4, 'Alt 4 EN', 'Alt 6 IT']);
|
||||
|
||||
// Check that we are not allowed to perform changes to multiple translations
|
||||
// in pending revisions when synchronized properties are involved.
|
||||
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
|
||||
$entity = $this->saveNewEntity();
|
||||
$entity_id = $entity->id();
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [21, 1, 1, 'Alt 1 EN']);
|
||||
|
||||
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||
$it_revision = $this->createRevision($it_translation);
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||
$metadata->setSource('en');
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [22, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
|
||||
|
||||
$en_revision = $this->createRevision($entity, FALSE);
|
||||
$en_revision->get($this->fieldName)->target_id = 2;
|
||||
$en_revision->getTranslation('it')->get($this->fieldName)->alt = 'Alt 3 IT';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertViolations($violations);
|
||||
|
||||
// Test that when saving a new default revision starting from a pending
|
||||
// revision, outdated synchronized properties do not override more recent
|
||||
// ones.
|
||||
$this->setUntranslatableFieldWidgetsDisplay(TRUE);
|
||||
$entity = $this->saveNewEntity();
|
||||
$entity_id = $entity->id();
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [23, 1, 1, 'Alt 1 EN']);
|
||||
|
||||
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||
$it_revision = $this->createRevision($it_translation, FALSE);
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||
$metadata->setSource('en');
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [24, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
|
||||
$en_revision = $this->createRevision($entity);
|
||||
$en_revision->get($this->fieldName)->target_id = 3;
|
||||
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
|
||||
$violations = $en_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($en_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [25, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
|
||||
|
||||
$it_revision = $this->createRevision($it_revision);
|
||||
$it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
|
||||
$violations = $it_revision->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($it_revision);
|
||||
$this->assertLatestRevisionFieldValues($entity_id, [26, 3, 3, 'Alt 3 EN', 'Alt 4 IT']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that file field synchronization works as expected.
|
||||
*/
|
||||
public function testFileFieldSynchronization(): void {
|
||||
$entity_type_id = 'entity_test_mulrev';
|
||||
$file_field_name = 'file_field';
|
||||
|
||||
foreach ($this->getTestFiles('text') as $file) {
|
||||
$entity = File::create((array) $file + ['status' => 1]);
|
||||
$entity->save();
|
||||
}
|
||||
|
||||
/** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
|
||||
$field_storage_config = FieldStorageConfig::create([
|
||||
'field_name' => $file_field_name,
|
||||
'type' => 'file',
|
||||
'entity_type' => $entity_type_id,
|
||||
'cardinality' => 1,
|
||||
'translatable' => 1,
|
||||
]);
|
||||
$field_storage_config->save();
|
||||
|
||||
$field_config = FieldConfig::create([
|
||||
'entity_type' => $entity_type_id,
|
||||
'field_name' => $file_field_name,
|
||||
'bundle' => $entity_type_id,
|
||||
'label' => 'Synchronized file field',
|
||||
'translatable' => 1,
|
||||
]);
|
||||
$field_config->save();
|
||||
|
||||
$property_settings = [
|
||||
'display' => 'display',
|
||||
'description' => 'description',
|
||||
'target_id' => 0,
|
||||
];
|
||||
$field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings);
|
||||
$field_config->save();
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = EntityTestMulRev::create([
|
||||
'uid' => 1,
|
||||
'langcode' => 'en',
|
||||
$file_field_name => [
|
||||
'target_id' => 1,
|
||||
'description' => 'Description EN',
|
||||
'display' => 1,
|
||||
],
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
$this->assertEquals(1, $entity->get($file_field_name)->target_id);
|
||||
$this->assertEquals('Description EN', $entity->get($file_field_name)->description);
|
||||
$this->assertEquals(1, $entity->get($file_field_name)->display);
|
||||
|
||||
// Create a translation with a different file, description and display
|
||||
// values.
|
||||
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||
$it_translation->get($file_field_name)->target_id = 2;
|
||||
$it_translation->get($file_field_name)->description = 'Description IT';
|
||||
$it_translation->get($file_field_name)->display = 0;
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_translation);
|
||||
$metadata->setSource('en');
|
||||
$it_translation->save();
|
||||
|
||||
$it_entity = $entity->getTranslation('it');
|
||||
$this->assertEquals(2, $it_entity->get($file_field_name)->target_id);
|
||||
$this->assertEquals('Description IT', $it_entity->get($file_field_name)->description);
|
||||
$this->assertEquals(0, $it_entity->get($file_field_name)->display);
|
||||
|
||||
// In the english entity the file should have changed, but the description
|
||||
// and display should have remained the same.
|
||||
$en_entity = $entity->getTranslation('en');
|
||||
$this->assertEquals(2, $en_entity->get($file_field_name)->target_id);
|
||||
$this->assertEquals('Description EN', $en_entity->get($file_field_name)->description);
|
||||
$this->assertEquals(1, $en_entity->get($file_field_name)->display);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests changing the default language of an entity.
|
||||
*/
|
||||
public function testChangeDefaultLanguageNonTranslatableFieldsHidden(): void {
|
||||
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
|
||||
$entity = $this->saveNewEntity();
|
||||
$entity->langcode = 'it';
|
||||
$this->assertCount(0, $entity->validate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets untranslatable field widgets' display status.
|
||||
*
|
||||
* @param bool $display
|
||||
* Whether untranslatable field widgets should be displayed.
|
||||
*/
|
||||
protected function setUntranslatableFieldWidgetsDisplay($display): void {
|
||||
$entity_type_id = $this->storage->getEntityTypeId();
|
||||
$settings = ['untranslatable_fields_hide' => !$display];
|
||||
$this->contentTranslationManager->setBundleTranslationSettings($entity_type_id, $entity_type_id, $settings);
|
||||
/** @var \Drupal\Core\Entity\EntityTypeBundleInfo $bundle_info */
|
||||
$bundle_info = $this->container->get('entity_type.bundle.info');
|
||||
$bundle_info->clearCachedBundles();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* The saved entity.
|
||||
*/
|
||||
protected function saveNewEntity() {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = EntityTestMulRev::create([
|
||||
'uid' => 1,
|
||||
'langcode' => 'en',
|
||||
$this->fieldName => [
|
||||
'target_id' => 1,
|
||||
'alt' => 'Alt 1 EN',
|
||||
],
|
||||
]);
|
||||
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
|
||||
$metadata->setSource(LanguageInterface::LANGCODE_NOT_SPECIFIED);
|
||||
$violations = $entity->validate();
|
||||
$this->assertEmpty($violations);
|
||||
$this->storage->save($entity);
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new revision starting from the latest translation-affecting one.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $translation
|
||||
* The translation to be revisioned.
|
||||
* @param bool $default
|
||||
* (optional) Whether the new revision should be marked as default. Defaults
|
||||
* to TRUE.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* An entity revision object.
|
||||
*/
|
||||
protected function createRevision(ContentEntityInterface $translation, $default = TRUE) {
|
||||
if (!$translation->isNewTranslation()) {
|
||||
$langcode = $translation->language()->getId();
|
||||
$revision_id = $this->storage->getLatestTranslationAffectedRevisionId($translation->id(), $langcode);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||
$revision = $this->storage->loadRevision($revision_id);
|
||||
$translation = $revision->getTranslation($langcode);
|
||||
}
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||
$revision = $this->storage->createRevision($translation, $default);
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the expected violations were found.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
|
||||
* A list of violations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertViolations(EntityConstraintViolationListInterface $violations): void {
|
||||
$entity_type_id = $this->storage->getEntityTypeId();
|
||||
$settings = $this->contentTranslationManager->getBundleTranslationSettings($entity_type_id, $entity_type_id);
|
||||
$message = !empty($settings['untranslatable_fields_hide']) ?
|
||||
'Non-translatable field elements can only be changed when updating the original language.' :
|
||||
'Non-translatable field elements can only be changed when updating the current revision.';
|
||||
|
||||
$list = [];
|
||||
foreach ($violations as $violation) {
|
||||
if ((string) $violation->getMessage() === $message) {
|
||||
$list[] = $violation;
|
||||
}
|
||||
}
|
||||
$this->assertCount(1, $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the latest revision has the expected field values.
|
||||
*
|
||||
* @param string $entity_id
|
||||
* The entity ID.
|
||||
* @param array $expected_values
|
||||
* An array of expected values in the following order:
|
||||
* - revision ID
|
||||
* - target ID (en)
|
||||
* - target ID (it)
|
||||
* - alt (en)
|
||||
* - alt (it)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertLatestRevisionFieldValues(string $entity_id, array $expected_values): void {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->storage->loadRevision($this->storage->getLatestRevisionId($entity_id));
|
||||
@[$revision_id, $target_id_en, $target_id_it, $alt_en, $alt_it] = $expected_values;
|
||||
$this->assertEquals($revision_id, $entity->getRevisionId());
|
||||
$this->assertEquals($target_id_en, $entity->get($this->fieldName)->target_id);
|
||||
$this->assertEquals($alt_en, $entity->get($this->fieldName)->alt);
|
||||
if ($entity->hasTranslation('it')) {
|
||||
$it_translation = $entity->getTranslation('it');
|
||||
$this->assertEquals($target_id_it, $it_translation->get($this->fieldName)->target_id);
|
||||
$this->assertEquals($alt_it, $it_translation->get($this->fieldName)->alt);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\content_translation\Kernel;
|
||||
|
||||
use Drupal\Core\Form\FormState;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests the content translation handler.
|
||||
*
|
||||
* @group content_translation
|
||||
*
|
||||
* @coversDefaultClass \Drupal\content_translation\ContentTranslationHandler
|
||||
*/
|
||||
class ContentTranslationHandlerTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'content_translation',
|
||||
'entity_test',
|
||||
'language',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* The state service.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The entity type bundle information.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The messenger.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* The ID of the entity type used in this test.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId = 'entity_test_mul';
|
||||
|
||||
/**
|
||||
* The ID of the translation language used in this test.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $translationLangcode = 'af';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->state = $this->container->get('state');
|
||||
$this->entityTypeBundleInfo = $this->container->get('entity_type.bundle.info');
|
||||
$this->entityTypeManager = $this->container->get('entity_type.manager');
|
||||
$this->messenger = $this->container->get('messenger');
|
||||
|
||||
$this->installEntitySchema($this->entityTypeId);
|
||||
ConfigurableLanguage::createFromLangcode($this->translationLangcode)->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests ContentTranslationHandler::entityFormSharedElements()
|
||||
*
|
||||
* @param array $element
|
||||
* The element that will be altered.
|
||||
* @param bool $default_translation_affected
|
||||
* Whether or not only the default translation of the entity is affected.
|
||||
* @param bool $default_translation
|
||||
* Whether or not the entity is the default translation.
|
||||
* @param bool $translation_form
|
||||
* Whether or not the form is a translation form.
|
||||
* @param array $expected
|
||||
* The expected altered element.
|
||||
*
|
||||
* @dataProvider providerTestEntityFormSharedElements
|
||||
*
|
||||
* @covers ::entityFormSharedElements
|
||||
* @covers ::addTranslatabilityClue
|
||||
*/
|
||||
public function testEntityFormSharedElements(array $element, $default_translation_affected, $default_translation, $translation_form, array $expected): void {
|
||||
$this->state->set('entity_test.translation', TRUE);
|
||||
$this->state->set('entity_test.untranslatable_fields.default_translation_affected', $default_translation_affected);
|
||||
$this->entityTypeBundleInfo->clearCachedBundles();
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->entityTypeManager->getStorage($this->entityTypeId)->create();
|
||||
if (!$default_translation) {
|
||||
$entity = $entity->addTranslation($this->translationLangcode);
|
||||
}
|
||||
$entity->save();
|
||||
|
||||
$form_object = $this->entityTypeManager->getFormObject($this->entityTypeId, 'default');
|
||||
$form_object->setEntity($entity);
|
||||
|
||||
$form_state = new FormState();
|
||||
$form_state
|
||||
->addBuildInfo('callback_object', $form_object)
|
||||
->set(['content_translation', 'translation_form'], $translation_form);
|
||||
|
||||
$handler = $this->entityTypeManager->getHandler($this->entityTypeId, 'translation');
|
||||
$actual = $handler->entityFormSharedElements($element, $form_state, $element);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns test cases for ::testEntityFormSharedElements().
|
||||
*
|
||||
* @return array[]
|
||||
* An array of test cases, each one containing the element to alter, the
|
||||
* form state, and the expected altered element.
|
||||
*/
|
||||
public static function providerTestEntityFormSharedElements() {
|
||||
$tests = [];
|
||||
|
||||
$element = [];
|
||||
$tests['empty'] = [
|
||||
'element' => $element,
|
||||
'default_translation_affected' => TRUE,
|
||||
'default_translation' => TRUE,
|
||||
'translation_form' => FALSE,
|
||||
'expected' => $element,
|
||||
];
|
||||
|
||||
$element = [
|
||||
'#type' => 'textfield',
|
||||
];
|
||||
$tests['no-children'] = $tests['empty'];
|
||||
$tests['no-children']['element'] = $element;
|
||||
$tests['no-children']['expected'] = $element;
|
||||
|
||||
$element = [
|
||||
'test' => [
|
||||
'#type' => 'textfield',
|
||||
'#multilingual' => TRUE,
|
||||
],
|
||||
];
|
||||
$tests['multilingual'] = $tests['empty'];
|
||||
$tests['multilingual']['element'] = $element;
|
||||
$tests['multilingual']['expected'] = $element;
|
||||
|
||||
unset($element['test']['#multilingual']);
|
||||
$tests['no-title'] = $tests['empty'];
|
||||
$tests['no-title']['element'] = $element;
|
||||
$tests['no-title']['expected'] = $element;
|
||||
|
||||
$element['test']['#title'] = 'Test';
|
||||
$tests['no-translatability-clue'] = $tests['empty'];
|
||||
$tests['no-translatability-clue']['element'] = $element;
|
||||
$tests['no-translatability-clue']['expected'] = $element;
|
||||
|
||||
$expected = $element;
|
||||
$expected['test']['#title'] .= ' <span class="translation-entity-all-languages">(all languages)</span>';
|
||||
$tests['translatability-clue'] = $tests['no-translatability-clue'];
|
||||
$tests['translatability-clue']['default_translation_affected'] = FALSE;
|
||||
$tests['translatability-clue']['expected'] = $expected;
|
||||
|
||||
$ignored_types = [
|
||||
'actions',
|
||||
'details',
|
||||
'hidden',
|
||||
'link',
|
||||
'token',
|
||||
'value',
|
||||
'vertical_tabs',
|
||||
];
|
||||
foreach ($ignored_types as $ignored_type) {
|
||||
$element = [
|
||||
'test' => [
|
||||
'#type' => $ignored_type,
|
||||
'#title' => 'Test',
|
||||
],
|
||||
];
|
||||
$tests["ignore-$ignored_type"] = $tests['translatability-clue'];
|
||||
$tests["ignore-$ignored_type"]['element'] = $element;
|
||||
$tests["ignore-$ignored_type"]['expected'] = $element;
|
||||
}
|
||||
|
||||
$tests['unknown-field'] = $tests['no-translatability-clue'];
|
||||
$tests['unknown-field']['default_translation'] = FALSE;
|
||||
|
||||
$element = [
|
||||
'name' => [
|
||||
'#type' => 'textfield',
|
||||
],
|
||||
'hidden_fields_warning_message' => [
|
||||
'#theme' => 'status_messages',
|
||||
'#message_list' => [
|
||||
'warning' => [t('Fields that apply to all languages are hidden to avoid conflicting changes. <a href=":url">Edit them on the original language form</a>.')],
|
||||
],
|
||||
'#weight' => -100,
|
||||
'#status_headings' => [
|
||||
'warning' => t('Warning message'),
|
||||
],
|
||||
],
|
||||
];
|
||||
$expected = $element;
|
||||
$expected['name']['#access'] = FALSE;
|
||||
$tests['hide-untranslatable'] = $tests['unknown-field'];
|
||||
$tests['hide-untranslatable']['element'] = $element;
|
||||
$tests['hide-untranslatable']['expected'] = $expected;
|
||||
|
||||
$tests['no-translation-form'] = $tests['no-translatability-clue'];
|
||||
$tests['no-translation-form']['translation_form'] = FALSE;
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user