diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 60c1867b32..3911e7551d 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -20,6 +20,10 @@ */ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface { + use EntityChangesDetectionTrait { + getFieldsToSkipFromTranslationChangesCheck as traitGetFieldsToSkipFromTranslationChangesCheck; + } + /** * The plain data values of the contained fields. * @@ -1356,17 +1360,7 @@ public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, * An array of field names. */ protected function getFieldsToSkipFromTranslationChangesCheck() { - /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */ - $entity_type = $this->getEntityType(); - // A list of known revision metadata fields which should be skipped from - // the comparision. - $fields = [ - $entity_type->getKey('revision'), - 'revision_translation_affected', - ]; - $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys())); - - return $fields; + return $this->traitGetFieldsToSkipFromTranslationChangesCheck($this); } /** diff --git a/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php b/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php new file mode 100644 index 0000000000..e598e8c089 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityChangesDetectionTrait.php @@ -0,0 +1,36 @@ +getEntityType(); + + // A list of known revision metadata fields which should be skipped from + // the comparision. + $fields = [ + $entity_type->getKey('revision'), + $entity_type->getKey('revision_translation_affected'), + ]; + $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys())); + + return $fields; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php index 1b8a33d3c8..daeafdc9b9 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraint.php @@ -15,6 +15,6 @@ */ class EntityUntranslatableFieldsConstraint extends Constraint { - public $message = 'Fields shared among translations can be changed only in default revisions.'; + public $message = 'Non translatable fields can only be changed when updating the current revision in the original language.'; } diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php index d4bd32345f..58d31079a4 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php +++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityUntranslatableFieldsConstraintValidator.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Entity\Plugin\Validation\Constraint; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityChangesDetectionTrait; use Drupal\Core\Field\ChangedFieldItemList; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -12,6 +13,8 @@ */ class EntityUntranslatableFieldsConstraintValidator extends ConstraintValidator { + use EntityChangesDetectionTrait; + /** * {@inheritdoc} */ @@ -25,13 +28,13 @@ public function validate($entity, Constraint $constraint) { } // To avoid unintentional reverts and data losses, we forbid changes to - // untranslatable fields in pending revisions for multilingual entities, - // until we support conflict management. The only case where pending - // revisions are acceptable is when untranslatable fields affect only the - // default translation, in which case a pending revision contains only one - // affected translation. Even in this case, multiple translations would be - // affected in a single revision, if we allowed changes to untranslatable - // fields while editing non-default translations, so that is forbidden too. + // untranslatable fields in pending revisions for multilingual entities. The + // only case where changes in pending revisions are acceptable is when + // untranslatable fields affect only the default translation, in which case + // a pending revision contains only one affected translation. Even in this + // case, multiple translations would be affected in a single revision, if we + // allowed changes to untranslatable fields while editing non-default + // translations, so that is forbidden too. if ($this->hasUntranslatableFieldsChanges($entity)) { if ($entity->isDefaultTranslationAffectedOnly()) { foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) { @@ -90,29 +93,4 @@ protected function hasUntranslatableFieldsChanges(ContentEntityInterface $entity return FALSE; } - /** - * Returns an array of field names to skip when checking for changes. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * A content entity object. - * - * @return array - * An array of field names. - */ - protected function getFieldsToSkipFromTranslationChangesCheck(ContentEntityInterface $entity) { - /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */ - $entity_type = $entity->getEntityType(); - - // A list of known revision metadata fields which should be skipped from - // the comparision. - $fields = [ - $entity_type->getKey('revision'), - $entity_type->getKey('revision_translation_affected'), - 'revision_type', // FIXME - ]; - $fields = array_merge($fields, array_values($entity_type->getRevisionMetadataKeys())); - - return $fields; - } - } diff --git a/core/modules/content_translation/config/schema/content_translation.schema.yml b/core/modules/content_translation/config/schema/content_translation.schema.yml index 8cf6e2c8c4..73676724bd 100644 --- a/core/modules/content_translation/config/schema/content_translation.schema.yml +++ b/core/modules/content_translation/config/schema/content_translation.schema.yml @@ -18,17 +18,9 @@ language.content_settings.*.*.third_party.content_translation: enabled: type: boolean label: 'Content translation enabled' - -content_translation.settings: - type: config_object - label: 'Content translation settings' - mapping: - untranslatable_fields_hide: + bundle_settings: type: sequence - label: 'Entity types' + label: 'Content translation bundle settings' sequence: - type: sequence - label: 'Bundles' - sequence: - type: boolean - label: 'Hide shared fields on translation forms' + type: string + label: 'Bundle settings values' diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc index 2f8b834fe5..98a5d5cee1 100644 --- a/core/modules/content_translation/content_translation.admin.inc +++ b/core/modules/content_translation/content_translation.admin.inc @@ -5,6 +5,7 @@ * The content translation administration forms. */ +use Drupal\content_translation\BundleSettingsInterface; use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -95,7 +96,6 @@ function _content_translation_form_language_content_settings_form_alter(array &$ $entity_manager = Drupal::entityManager(); $bundle_info_service = \Drupal::service('entity_type.bundle.info'); - $config = \Drupal::configFactory()->get('content_translation.settings'); foreach ($form['#labels'] as $entity_type_id => $label) { $entity_type = $entity_manager->getDefinition($entity_type_id); $storage_definitions = $entity_type instanceof ContentEntityTypeInterface ? $entity_manager->getFieldStorageDefinitions($entity_type_id) : []; @@ -113,20 +113,21 @@ function _content_translation_form_language_content_settings_form_alter(array &$ } // Displayed the "shared fields widgets" toggle. - $key = 'untranslatable_fields_hide.' . $entity_type_id . '.' . $bundle; - $form['settings'][$entity_type_id][$bundle]['settings']['untranslatable_fields_hide'] = [ - '#type' => 'checkbox', - '#title' => t('Hide shared fields on translation forms'), - '#description' => t('This will prevent editors from changing field values shared among translations when editing content translations.'), - '#default_value' => $config->get($key), - '#states' => [ - 'visible' => [ - ':input[name="settings[' . $entity_type_id . '][' . $bundle . '][translatable]"]' => [ - 'checked' => TRUE, + if ($content_translation_manager instanceof BundleSettingsInterface) { + $settings = $content_translation_manager->getBundleSettings($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' => !empty($settings['untranslatable_fields_hide']), + '#states' => [ + 'visible' => [ + ':input[name="settings[' . $entity_type_id . '][' . $bundle . '][translatable]"]' => [ + 'checked' => TRUE, + ], ], ], - ], - ]; + ]; + } $fields = $entity_manager->getFieldDefinitions($entity_type_id, $bundle); if ($fields) { @@ -337,7 +338,6 @@ function content_translation_form_language_content_settings_validate(array $form function content_translation_form_language_content_settings_submit(array $form, FormStateInterface $form_state) { /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */ $manager = \Drupal::service('content_translation.manager'); - $config = \Drupal::configFactory()->getEditable('content_translation.settings'); $entity_types = $form_state->getValue('entity_types'); $settings = &$form_state->getValue('settings'); @@ -370,9 +370,10 @@ function content_translation_form_language_content_settings_submit(array $form, // Store whether a bundle has translation enabled or not. $manager->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']); - // TODO Should we use the content translation manager for this too? - $key = 'untranslatable_fields_hide.' . $entity_type_id . '.' . $bundle; - $config->set($key, (bool) $bundle_settings['settings']['untranslatable_fields_hide']); + // Store any other bundle settings. + if ($manager instanceof BundleSettingsInterface) { + $manager->setBundleSettings($entity_type_id, $bundle, $bundle_settings['settings']['content_translation']); + } // Save translation_sync settings. if (!empty($bundle_settings['columns'])) { @@ -393,8 +394,6 @@ function content_translation_form_language_content_settings_submit(array $form, } } - $config->save(); - // Ensure entity and menu router information are correctly rebuilt. \Drupal::entityManager()->clearCachedDefinitions(); \Drupal::service('router.builder')->setRebuildNeeded(); diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 3f53d7d598..a6db070e59 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -5,6 +5,7 @@ * Allows entities to be translated into different languages. */ +use Drupal\content_translation\BundleSettingsInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\ContentEntityFormInterface; use Drupal\Core\Entity\ContentEntityInterface; @@ -163,12 +164,13 @@ function content_translation_entity_type_alter(array &$entity_types) { function content_translation_entity_bundle_info_alter(&$bundles) { /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */ $manager = \Drupal::service('content_translation.manager'); - $config = \Drupal::configFactory()->get('content_translation.settings'); foreach ($bundles as $entity_type_id => &$info) { foreach ($info as $bundle => &$bundle_info) { $bundle_info['translatable'] = $manager->isEnabled($entity_type_id, $bundle); - $key = 'untranslatable_fields_hide.' . $entity_type_id . '.' . $bundle; - $bundle_info['untranslatable_fields.default_translation_affected'] = $config->get($key); + if ($manager instanceof BundleSettingsInterface) { + $settings = $manager->getBundleSettings($entity_type_id, $bundle); + $bundle_info['untranslatable_fields.default_translation_affected'] = !empty($settings['untranslatable_fields_hide']); + } } } } diff --git a/core/modules/content_translation/src/BundleSettingsInterface.php b/core/modules/content_translation/src/BundleSettingsInterface.php new file mode 100644 index 0000000000..f02522927e --- /dev/null +++ b/core/modules/content_translation/src/BundleSettingsInterface.php @@ -0,0 +1,35 @@ +entityTypeId = $entity_type->id(); $this->entityType = $entity_type; $this->languageManager = $language_manager; $this->manager = $manager; $this->currentUser = $current_user; $this->fieldStorageDefinitions = $entity_manager->getLastInstalledFieldStorageDefinitions($this->entityTypeId); + $this->messenger = $messenger; } /** @@ -102,7 +118,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $container->get('language_manager'), $container->get('content_translation.manager'), $container->get('entity.manager'), - $container->get('current_user') + $container->get('current_user'), + \Drupal::messenger() ); } @@ -269,6 +286,8 @@ public function getSourceLangcode(FormStateInterface $form_state) { * {@inheritdoc} */ public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $form_object = $form_state->getFormObject(); $form_langcode = $form_object->getFormLangcode($form_state); $entity_langcode = $entity->getUntranslated()->language()->getId(); @@ -517,7 +536,14 @@ public function entityFormSharedElements($element, FormStateInterface $form_stat /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $entity = $form_object->getEntity(); $display_translatability_clue = !$entity->isDefaultTranslationAffectedOnly(); - $hide_shared_fields = $entity->isDefaultTranslationAffectedOnly() && !$entity->isDefaultTranslation(); + $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, for + // instance the "revision_log" field, should not be affected by this logic. + $field_definitions = array_diff_key($entity->getFieldDefinitions(), array_flip($this->getFieldsToSkipFromTranslationChangesCheck($entity))); foreach (Element::children($element) as $key) { if (!isset($element[$key]['#type'])) { @@ -531,12 +557,17 @@ public function entityFormSharedElements($element, FormStateInterface $form_stat // 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 shared form elements should be - // hidden. - if (!$hide_shared_fields && !$form_state->get(['content_translation', 'translation_form'])) { + // 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; @@ -545,6 +576,11 @@ public function entityFormSharedElements($element, FormStateInterface $form_stat } } + if ($display_warning && !$form_state->isSubmitted() && !$form_state->isRebuilding()) { + $url = $entity->getUntranslated()->toUrl('edit-form')->toString(); + $this->messenger->addWarning($this->t('All fields that apply to all languages were hidden to avoid conflicting changes. You can edit them on the original language form.', ['@url' => $url])); + } + return $element; } diff --git a/core/modules/content_translation/src/ContentTranslationManager.php b/core/modules/content_translation/src/ContentTranslationManager.php index da8bd1c618..28acd42815 100644 --- a/core/modules/content_translation/src/ContentTranslationManager.php +++ b/core/modules/content_translation/src/ContentTranslationManager.php @@ -8,7 +8,7 @@ /** * Provides common functionality for content translation. */ -class ContentTranslationManager implements ContentTranslationManagerInterface { +class ContentTranslationManager implements ContentTranslationManagerInterface, BundleSettingsInterface { /** * The entity type manager. @@ -105,6 +105,23 @@ public function isEnabled($entity_type_id, $bundle = NULL) { return $enabled; } + /** + * {@inheritdoc} + */ + public function setBundleSettings($entity_type_id, $bundle, array $settings) { + $config = $this->loadContentLanguageSettings($entity_type_id, $bundle); + $config->setThirdPartySetting('content_translation', 'bundle_settings', $settings) + ->save(); + } + + /** + * {@inheritdoc} + */ + public function getBundleSettings($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. * diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationUntranslatableFieldsTest.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationUntranslatableFieldsTest.php index fa0dd01b60..e973470ace 100644 --- a/core/modules/content_translation/tests/src/Functional/ContentTranslationUntranslatableFieldsTest.php +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationUntranslatableFieldsTest.php @@ -47,7 +47,7 @@ protected function getEditorPermissions() { } /** - * Tests that hiding untransltable field widgets works correctly. + * Tests that hiding untranslatable field widgets works correctly. */ public function testHiddenWidgets() { /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ @@ -59,7 +59,7 @@ public function testHiddenWidgets() { ->load($id); // Check that the untranslatable field widget is displayed on the edit form - // and not translatability clue is displayed yet. + // and no translatability clue is displayed yet. $this->drupalGet($entity->toUrl('edit-form')); $field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]'; $this->assertNotEmpty($this->xpath($field_xpath)); @@ -67,8 +67,8 @@ public function testHiddenWidgets() { $this->assertEmpty($this->xpath($clue_xpath)); // Add a translation and check that the untranslatable field widget is - // displayed on the translation and edit forms along with the - // translatability clue. + // 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', @@ -92,7 +92,7 @@ public function testHiddenWidgets() { // Configure untranslatable field widgets to be hidden on non-default // language edit forms. $edit = [ - 'settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][untranslatable_fields_hide]' => 1, + 'settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][content_translation][untranslatable_fields_hide]' => 1, ]; $this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration'); @@ -107,5 +107,9 @@ public function testHiddenWidgets() { $this->drupalGet($entity->toUrl('edit-form', ['language' => $it_language])); $this->assertEmpty($this->xpath($field_xpath)); $this->assertEmpty($this->xpath($clue_xpath)); + + // Verify a warning is displayed. + $this->assertSession() + ->pageTextContains('All fields that apply to all languages were hidden to avoid conflicting changes. You can edit them on the original language form.'); } }