Problem/Motivation

In #2878556: Ensure that changes to untranslatable fields affect only one translation in pending revisions we introduced a new validation logic (coupled with complementary UI changes) that ensures that no changes to untranslatable fields can be made in a way that would cause multiple revision translations to be marked as affected in a single pending revision.

In #2878556-16: Ensure that changes to untranslatable fields affect only one translation in pending revisions it was brought up that translatable fields having field property synchronization enabled may run into the same issues, because if an untranslatable field property is changed in any revision translation, the same changes are propagated to all the other translations on presave. This causes all revision translations to be marked as affected, which is exactly what the parent issue aimed to prevent.

Proposed resolution

@effulgentsia's in #5:

For purposes of unblocking #2860097: Ensure that content translations can be moderated independently, what if all we did was still show the full widget (with both its translatable and untranslatable properties/elements), but we add a validation error if the translator actually tries to change an untranslatable element?
[…] it would prevent getting into the data integrity problem reported in #2860097-153: Ensure that content translations can be moderated independently.

@plach implemented this in #6.

Remaining tasks

  • Find a viable solution A validator
  • Write a patch See #6
  • Reviews

User interface changes

A new validation error to prevent data integrity problems, only in those cases where there would otherwise have been data integrity problems.

API changes

None.

Data model changes

None.

Comments

plach created an issue. See original summary.

plach’s picture

I think that ideally we would like the UI to behave exactly as it does for untranslatable fields: in the default mode we would prevent changes to fields having field property synchronization enabled, while in the alternative mode we would hide (or disable?) only the form element related to the untranslatable property (the upload item in the case of an image field), while leaving the other ones (alt/title) editable, since those changes would affect only the current translation.

However I'm not sure how easy it is to implement such a solution in a generic way. I'm wondering whether there are acceptable alternative solutions we could implement.

plach’s picture

Title: Translatable fields with synchronization enable should behave as untranslatable fields with respect to pending revisions » Translatable fields with synchronization enabled should behave as untranslatable fields with respect to pending revisions
matsbla’s picture

in the alternative mode we would hide (or disable?) only the form element related to the untranslatable property (the upload item in the case of an image field), while leaving the other ones (alt/title) editable

My humble opinion is that this should be the default mode. If not it will not be possible to edit alt/title attributes in a draft, you can only change them if you directly publish them. My guess is that many users will even consider that as a bug, and most users would like to be able to edit translations of alt/title attributes in drafts just like they edit any other translatable fields.

effulgentsia’s picture

My humble opinion is that this should be the default mode.

That's out of scope for this issue. The "Hide non translatable fields on translation forms" checkbox on /admin/config/regional/content-language was already added to core in #2878556: Ensure that changes to untranslatable fields affect only one translation in pending revisions with a default state of off. But let's perhaps open a new issue to discuss the pros and cons of changing that to default to on when content_moderation is enabled?

I think for this issue, we just need to make sure that the behavior and validation that's conditional on that checkbox also applies to non-translatable properties within translatable fields.

we would hide (or disable?) only the form element related to the untranslatable property... while leaving the other ones ...editable

I agree that that would be the expected behavior for when that checkbox is checked.

However I'm not sure how easy it is to implement such a solution in a generic way.

That's worth exploring. It's a similar problem as what is solved by WidgetBaseInterface::flagErrors(), but a little different. We may need to expand that API to cover other cases of mapping properties to widget sub-elements, such as this case of the translation form needing to hide/disable some of those subelements but not others.

I'm wondering whether there are acceptable alternative solutions we could implement.

For purposes of unblocking #2860097: Ensure that content translations can be moderated independently, what if all we did was still show the full widget (with both its translatable and untranslatable properties/elements), but we add a validation error if the translator actually tries to change an untranslatable element? It's not the most ideal solution from a usability standpoint, but at least in the meantime, until we get a better UI in place, it would prevent getting into the data integrity problem reported in #2860097-153: Ensure that content translations can be moderated independently.

plach’s picture

Status: Active » Needs review
StatusFileSize
new20.53 KB

This implements a validator that prevents the entity form being saved if a synchronized property is changed while attempting to save a pending revision.

wim leers’s picture

Issue summary: View changes

So #6 implements @effulgentsia's suggestion at the end of #5:

For purposes of unblocking #2860097: Ensure that content translations can be moderated independently, what if all we did was still show the full widget (with both its translatable and untranslatable properties/elements), but we add a validation error if the translator actually tries to change an untranslatable element?
[…] it would prevent getting into the data integrity problem reported in #2860097-153: Ensure that content translations can be moderated independently.

Updated the IS for that.

wim leers’s picture

I would like to see a failing test-only patch. So I created that. Upon walking through that locally, I first thought that this:

  public function testPendingRevisionValidation() {
    foreach ([TRUE, FALSE] as $untranslatable_field_widget_display) {
      $this->doTestPendingRevisionValidation($untranslatable_field_widget_display);
    }
  }

meant that in one case, we expect a validation error, and in the other we don't. I was wrong. What this is testing, is that we get this validation error regardless of whether the Hide non translatable fields on translation forms (untranslatable_fields_hide) setting is enabled for a particular bundle. In other words: testing with that setting on and off ensures that this validation is error is triggered regardless of configuration; the validation error is triggered when a particular set of changes in properties is attempted.

At least, that is my understanding, with my limited background about how all this works or is supposed to work. Hopefully that helps the next reviewer.


Patch review:

  1. +++ b/core/modules/content_translation/src/ExtendedFieldTranslationSynchronizerInterface.php
    @@ -0,0 +1,37 @@
    + * @internal
    ...
    +interface ExtendedFieldTranslationSynchronizerInterface extends FieldTranslationSynchronizerInterface {
    
    +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -5,11 +5,12 @@
    -class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterface {
    +class FieldTranslationSynchronizer implements ExtendedFieldTranslationSynchronizerInterface {
    
    +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php
    @@ -0,0 +1,130 @@
    +    if (!$this->synchronizer instanceof ExtendedFieldTranslationSynchronizerInterface) {
    

    I was going to ask why this should be internal, and why this should be a new interface, because https://www.drupal.org/core/d8-bc-policy#interfaces allows us to update the existing interface.

    But I think I already found the answer: the validator is now able to check for this interface, and therefore this validator only has effect for this one implementation. Other implementations are allowed, but are warned that the implementation may change, since it's marked @internal. A "proper" API may follow in the future.

  2. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -28,6 +29,45 @@ public function __construct(EntityManagerInterface $entityManager) {
    +    return $field_definition instanceof ThirdPartySettingsInterface &&  $field_definition->isTranslatable() &&
    +    ($translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync')) ? $translation_sync : NULL;
    

    Nit: s/&& /&& /

    Also: that is one very very very long return statement. Can we split it up for legibility?

  3. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraint.php
    @@ -0,0 +1,22 @@
    +  public $message = 'Non translatable field elements can only be changed when updating the current revision';
    

    Nits:
    s/Non translatable/Non-translatable/
    s/field elements/fields/ (because validators don't run only for entities changed via forms, they run for entities being changed from any place)

  4. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php
    @@ -0,0 +1,130 @@
    +    if ($entity->isNew() || $entity->isDefaultRevision() || !$entity->getEntityType()->isRevisionable()) {
    

    This effectively is returning early when it's not a pending revision, because we only want to protect the case of a pending revision in this validator.

    I think a comment to that effect here would go a long way in making this more understandable/maintainable in the future.

effulgentsia’s picture

+++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraint.php
@@ -0,0 +1,22 @@
+  public $message = 'Non translatable field elements can only be changed when updating the current revision';

The validator implements what this message says. But is that what's desired? This means that I can't use content_moderation to work on a draft of a node, and while working on that draft, change the image that I want in the image field. Even when I'm editing in the entity's original language, not a translation.

This is in contrast to the EntityUntranslatableFieldsConstraint that was committed in #2878556: Ensure that changes to untranslatable fields affect only one translation in pending revisions, which says:

public $message = 'Non translatable fields can only be changed when updating the current revision or the original language.';

Seems to me that that "or the original language" is an important feature to retain, even for field elements, not just fields.

gábor hojtsy’s picture

Agreed with @effulgentsia on wording. Also @timmillwood pointed out elsewhere that "Non translatable" should be "Non-translatable" I grepped core, and there are more hyphenated than non-hyphenated. Native English speaker feedback would be nice :) We'll need an issue to clean this up even after this one. (Minor).

plach’s picture

StatusFileSize
new24.93 KB
new33.85 KB

Here's a new patch addressing the reviews above. Working on a comment with a few replies.

Once again the interdiff is basically useless :)

wim leers’s picture

Things that weren't clear + nits in a first review pass over this patch:

  1. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -88,6 +90,24 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +      // If changes to untranslatable fields affect are configured to affect
    

    s/affect are/are/

  2. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -88,6 +90,24 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +      // is pending revisions, otherwise multiple revision translations would be
    

    s/is/in/

  3. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -88,6 +90,24 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +      // to make sure that we reconcile any previous changes in the default
    ...
    +        $sync_langcode = $entity->getUntranslated()->language()->getId();
    +        $force_sync = TRUE;
    

    Nit: "reconcile" vs "sync" — why not also say "sync" in the comment? Different terminology suggests different things/processes are at play here, introducing doubt when trying to understand this (at least for me).

  4. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -140,6 +164,29 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +   * Ensures that field values are actually synchronized.
    

    Syncs the field values of all given columns.

  5. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -140,6 +164,29 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +   * @param string $sync_langcode
    +   *   The
    

    Incomplete comment.

  6. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -140,6 +164,29 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +  protected function enforceSynchronization(array &$field_values, $sync_langcode, array $columns) {
    ...
    +          foreach ($columns as $column) {
    

    Shouldn't "columns" here be "properties"?

  7. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraint.php
    @@ -17,6 +17,6 @@
    -  public $message = 'Non translatable field elements can only be changed when updating the current revision';
    +  public $message = 'Non-translatable field elements can only be changed when updating the current revision or the original language.';
    

    See #8.3, second nit.

  8. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php
    @@ -13,7 +13,21 @@
    + *   translations, synchronized field properties can be changed only in default
    

    It says "properties" here, but "column" above. See prior point.

  9. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php
    @@ -13,7 +13,21 @@
    + *   the default revision translation, synchronized field properties can be
    

    "default revision translation" is hard to interpret. Is it "the default revision of a translation" or "the default translation of a revision"?

    Either of those would be clearer than what's currently there.

  10. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php
    @@ -13,7 +13,21 @@
    + *   reconciled when saving changes to the default translation as the new
    

    "reconciled" here again, see prior remark.

  11. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php
    @@ -90,7 +109,31 @@ public function validate($value, Constraint $constraint) {
    +            // We already checked the active translation above.
    

    I don't see anything about "active" translation above, only about the "default" translation. Are those things the same?

plach’s picture

Thanks for the updates and reviews!

@Wim Leers, #8:

I did not post a test only patch because assertions are relying on a validator that does not exist in HEAD, so failures are not telling a lot IMHO. Anyway, your analysis was correct, although the new patch completely changes that approach.

Patch review:
1: Correct, I want freedom to revamp this logic if we need to.
2: Fixed
3: In this case "elements" refers to "field properties" not "fields", it's what we are using in the UI elsewhere, so I think it makes sense to keep it. Basically prioritizing UX over DX ;)
4: Fixed and added an intro in the PHP docblock. Hope it's fine now ;)


@effulgentsia, #10:

The reason why I did not implement that in #6 was that (in the second mode) untranslatable fields are not causing non-default revision translations to be marked as affected, even when they change. OTOH this would not be possible with synchronized properties, because those are regular translatable fields for the RTA flag populate logic. However, after mulling a bit on that, I realized we just need to skip synchronization to mimic the same behavior for synchronized properties. This is what #10 implements.


Edit: the only relevant change is in bold.

effulgentsia’s picture

Adding credit to @matsbla for discovering the bug: thank you!

@matsbla: Can you confirm if this patch (in conjunction with #2860097: Ensure that content translations can be moderated independently) fixes everything that you discovered?

plach’s picture

Please note that the patch needs to be tested in combination with #2860097: Ensure that content translations can be moderated independently to get meaningful results from the UI.

plach’s picture

StatusFileSize
new8.77 KB
new33.92 KB

@Wim Leers, #13:

1-5: Fixed
6/8: Yep, columns is an obsolete terminology that (unnecessarily) refers to the storage. We have been referring to field properties for a while when talking about those, but all the low level stuff still uses column. Updated the patch to use column instead of property for consistency.
9-10: Fixed
11: Active translation is what we usually call the translation object being passed around, it may or may not be the default translation.

wim leers’s picture

Issue tags: +Entity Field API

The patch looks good. But it now goes so deep into Entity/Field API land that it makes me unable to sign off on this. Hoping @effulgentsia or another Entity/Field API maintainer can RTBC this.

matsbla’s picture

Really great to see so rapid fixes on this!

I tested the patches and the validation error works as expected.

However I did find another issue. The problem now is when you've enabled that checkbox ("Hide non translatable fields on translation forms") which enable you to save drafts in original language where you can change a non-translatable image file (which is great and makes sense). Once you now publish that draft it seems to me like that image file is not being synchronized. So only the original language gets that image file, whereas all the other languages will keep the former image file. The synchronization seems to work only if you publish directly, but not if you publish a draft.

Also it seems to me that when you have a pending draft where you made changes to non-translatable image files it is not possible to add drafts for translations into new languages anymore; when you do that the form is loaded with the most recent draft of the original language (containing the new not published non-translatable image file, which you're not allowed to save as a draft in translations). I'm not sure if this is by design, but for me it seem a little bit strange that I'm going to start translate from the draft in review and not the published version of the original language.

effulgentsia’s picture

Status: Needs review » Needs work

Thanks for #19! For others looking at this, here's the steps to reproduce it:

  1. Install Standard Profile from 8.6.x branch tip + #2860097-157: Ensure that content translations can be moderated independently + #17.
  2. Enable Content Translation and Content Moderation modules.
  3. Add a 2nd language to the site.
  4. Configure CT to apply to Articles and check the "Hide non translatable fields on translation forms" checkbox.
  5. Configure CM's Editorial workflow to apply to Articles.
  6. Create an article node with an image and publish it.
  7. Create a translation of it and publish it.
  8. Edit that node in its original language, change the image, and save the node as Published.
  9. View the node in the 2nd language and verify that the image updated there as well.
  10. Edit the node again in its original language, change the image, and save the node as Draft.
  11. Edit it again in its original language, change nothing, and save it as Published.
  12. View the node in the 2nd language. Expected behavior: the image should be updated to what was uploaded in step 10. Actual behavior: it's still the same as it was in step 9.

The reason for this looks to be:

+++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
@@ -92,6 +151,10 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
             $unchanged_items = $entity_unchanged->getTranslation($langcode)->get($field_name)->getValue();
             $this->synchronizeItems($values, $unchanged_items, $sync_langcode, array_keys($translations), $columns);

These lines are unchanged in the patch, but it looks like $unchanged_items is based solely on the 1st language, so in step 8 it's different than what's in $values, and therefore synchronizeItems() synchronizes that difference to the 2nd language. However, in step 11, it's the same (since step 10 and step 11 are done in two separate steps), so synchronizeItems() doesn't synchronize the new image to the 2nd language.

effulgentsia’s picture

+++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
@@ -101,6 +164,29 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
+  protected function enforceSynchronization(array &$field_values, $sync_langcode, array $columns) {
+    $source_items = $field_values[$sync_langcode];
+    foreach ($field_values as $langcode => &$items) {
+      if ($langcode !== $sync_langcode) {
+        foreach ($items as $delta => $item) {
+          foreach ($columns as $column) {
+            $items[$delta][$column] = $source_items[$delta][$column];
+          }
+        }
+      }
+    }
+  }

I haven't tested the code path for this, but it strikes me as odd that the implementation is so different than in synchronizeItems(). Seems like unlike the existing code in synchronizeItems(), this new code is assuming that prior to synchronization all languages have the same number and order of items? My hunch is that that's not a correct assumption, but if it is, why is it?

effulgentsia’s picture

+++ b/core/modules/content_translation/src/ExtendedFieldTranslationSynchronizerInterface.php
@@ -0,0 +1,37 @@
+interface ExtendedFieldTranslationSynchronizerInterface extends FieldTranslationSynchronizerInterface {

I don't think we should add a new interface. I think we need to add methods to FieldTranslationSynchronizerInterface itself. Our BC policy allows that. I think it's preferable for modules/sites that implement a custom synchronizer (do any actually exist?) to have PHP give them a fatal error pointing to them to needing to add the new method(s) rather than having those sites silently not having this validator run.

+++ b/core/modules/content_translation/src/ExtendedFieldTranslationSynchronizerInterface.php
@@ -0,0 +1,37 @@
+  public function getOriginalEntity(ContentEntityInterface $entity);

I don't think this belongs on this interface (or on FieldTranslationSynchronizerInterface). Perhaps we should open an issue to add this to some interface within Drupal\Core\Entity? In the meantime, could we either duplicate this as a protected method in the places that need it or add a @internal trait if we want to avoid that duplication?

+++ b/core/modules/content_translation/src/ExtendedFieldTranslationSynchronizerInterface.php
@@ -0,0 +1,37 @@
+  public function getSynchronizedFieldSettings(array $field_definitions);

Something like this probably does belong on FieldTranslationSynchronizerInterface. But can we make it a narrower API? Otherwise, it's just a dumping bag of whatever is in the config variable, which we shouldn't be leaking to callers that don't need to care about that internal structure. How about getSynchronizedProperties() or getPropertiesToSynchronize()? Passed a single field definition (not an array, because the caller can call it for each field that it's interested in separately). And where the implementation is responsible for dealing with "groups" as an internal implementation detail: the caller just gets an array of the property/column names that need synchronizing. I think for purposes of naming the method, "properties" is better than "columns", since the "column" terminology is a historical implementation artifact, not something we need to leak to the API. As a separate issue, perhaps we want to rename the $columns parameter in synchronizeItems() to $properties, since I think in PHP, renaming a parameter name is neither an API change nor a BC break?

effulgentsia’s picture

+++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php
@@ -0,0 +1,203 @@
+    // 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;
+    }

Why not allow changes to default revisions regardless of isDefaultTranslationAffectedOnly() like EntityUntranslatableFieldsConstraintValidator does?

plach’s picture

StatusFileSize
new31.26 KB
new41.17 KB

@matsbla:

Thanks for manual testing this! The attached patch should address #19/#20.


@effulgentsia, #20:

Actually the reason was that we were enforcing synchronization only when saving a default revision for a non-default translation. In fact enforcing sync is required exactly because in such a scenario, no change would be detected, since we are switching the synchronization language.


@#21:

Field synchronization aims to make translatable fields behave exactly as untranslatable fields (except for textual properties, that can be changed per translation). For this reason it will make sure that every translation has the same number of items and in the same order. For this is reason, once items are synchronized, it's perfectly safe to make those assumptions.


@#22:

I don't think we should add a new interface. [...]

Fair enough, the goal was to be free to revamp it when working on a proper fix, but this patch has reached a level of polishment that no longer justifies that approach.

I don't think this belongs on this interface [...]

Good point. My original plan was to merge the ::getOriginalTranslation() logic in that method, but then it turned out not to be needed, so fine with making it protected. We have a series of issues aiming to clean-up / fix the original entity handling, so no point in doing more work here. See #2839195: Add a method to access the original property and #2833084: $entity->original doesn't adequately address intentions when saving a revision.

Something like this probably does belong on [...]

Fixed.

As a separate issue, perhaps we want to rename the $columns parameter in synchronizeItems() to $properties, since I think in PHP, renaming a parameter name is neither an API change nor a BC break?

Sounds like a good idea for a novice issue ;)


@#23:

We can't do that, it's the logic in EntityUntranslatableFieldsConstraintValidator that's buggy, I didn't realize that earlier because field widgets are hidden in that case, but here's an example of what might happen if widgets weren't hidden:

  1. Create an English article and publish it
  2. Create an Italian translation and publish it
  3. Save an English draft with untranslatable fields/synchronized property changes
  4. Save a new Italian default revision with untranslatable fields/synchronized property changes
  5. Publish the English draft -> you just overrode the Italian changes

Depending on the selected mode, you can only perform untranslatable field/sync property changes to the default revision XOR the default translation. I updated the constraint messages to make that clearer. Gabor suggested something similar in #2878556: Ensure that changes to untranslatable fields affect only one translation in pending revisions, but I think we forgot to implement it. We probably need a follow-up to fix EntityUntranslatableFieldsConstraintValidator as well, although it's probably not super urgent.

plach’s picture

Status: Needs work » Needs review
plach’s picture

StatusFileSize
new3.05 KB
new41.36 KB

Small optimization.

wim leers’s picture

Interdiff review:

  1. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -19,55 +20,57 @@ class FieldTranslationSynchronizer implements ExtendedFieldTranslationSynchroniz
    -      return $field_definition->getThirdPartySetting('content_translation', 'translation_sync') ?: NULL;
    +      return $field_definition->getThirdPartySetting('content_translation', 'translation_sync') ?: [];
    

    Nit: can't we change this to:

    return $this->…->getThirdPartySetting(…, …, []);
    

    i.e. use the third parameter, $default, to specify the default we want?

  2. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraint.php
    @@ -17,6 +17,8 @@
    +  public $defaultRevisionMessage = 'Non-translatable field elements can only be changed when updating the current revision.';
    

    Can we add a comment here to document what "elements" means in this context? @plach explained this in #14.

  3. +++ b/core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php
    @@ -102,20 +100,20 @@ public function validate($value, Constraint $constraint) {
             if ($has_changes) {
    -          $this->context->addViolation($constraint->message);
    +          $this->context->addViolation($constraint->defaultTranslationMessage);
             }
    
    @@ -134,49 +132,63 @@ public function validate($value, Constraint $constraint) {
         elseif ($has_changes) {
    -      $this->context->addViolation($constraint->message);
    +      $this->context->addViolation($constraint->defaultRevisionMessage);
         }
    

    Nice! 👌 Different execution branches, different messages, makes much more sense!


(patch review)

  1. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -43,21 +87,39 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +      // default revision translation, we need to skip synchronization in
    

    s/default revision translation/default translation/, because that's what \Drupal\Core\Entity\TranslatableRevisionableInterface::isDefaultTranslationAffectedOnly() is about.

  2. +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -92,6 +154,10 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +            if ($force_sync) {
    +              $this->enforceSynchronization($values, $sync_langcode, $columns);
    +            }
    
    @@ -101,6 +167,49 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    +   * Syncs the field values of all specified properties.
    +   *
    

    @plach just explained to me in a call why this is A) a separate method (i.e. not part of synchronizeItems()), B) why its code can be so much simpler (than synchronizeItems()).

    The short version of A is: because changing synchronizeItems() is relatively risky (despite lots of existing test coverage).

    The short version of B is: precisely because this runs after synchronizeItems(), a lot of the preparation work in terms of ordering and ensuring a clean data structure is already done.

    This should be documented, probably on the docblock for enforceSynchronization().

wim leers’s picture

@plach asked me to review, so I did my best … but I fear that my lack of deep knowledge about the pre-existing architecture is getting in the way.


#24

  • #19/#20 fix: I see you added explicit test coverage, excellent!
  • #20: so you're referring to the fix, which is this change:
    +++ b/core/modules/content_translation/src/FieldTranslationSynchronizer.php
    @@ -99,10 +101,11 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
    -      // When saving a default revision for a non-default translation, we need
    -      // to make sure that we enforce synchronization of any previous changes in
    -      // the default translation.
    -      elseif (!$entity->isDefaultTranslation()) {
    +      // When saving a default revision, we need to make sure that we enforce
    +      // synchronization of any previous changes in the default translation,
    +      // since a pending revision may have desynchronized field values, even if
    +      // there is no change between the current and the previous revision.
    +      else {
    

    I think the comment here could be clearer, but maybe that's just me not having enough familiarity with this area.

  • #21: @plach also explained this to me in a call, thanks to which I was able to understand this mostly (intuitively rather than exactly), this is why I asked for an explanation in protected function enforceSynchronizatino()'s docblock, at the very end of my previous comment.
  • #22: 👍
  • #23: We probably need a follow-up to fix EntityUntranslatableFieldsConstraintValidator as well, although it's probably not super urgent. Can we get a follow-up for this?

Hopefully @plach's comment 24 (which addressed @effulgentsia's feedback) and my feedback in this comment and my previous comment will result in one more reroll by @plach that will allow @effulgentsia to RTBC!

plach’s picture

StatusFileSize
new7.21 KB
new42.38 KB

This should address #27/#28.

After discussing this with Wim, I was able to find an alternative solution to get rid of the enforceSynchronization() method altogether, so parts of his review did not apply anymore.

plach’s picture

StatusFileSize
new1.5 KB
new42.46 KB

And now with more $property sauce!

plach’s picture

StatusFileSize
new5.62 KB
new43.13 KB

I performed a live review of this with @effulgentsia and he spotted a problem in how we were handling changes in multiple translation in a single revision.

plach’s picture

effulgentsia’s picture

#33 is looking good to me. I just want to add/change a few code comments. I'll post that within the next few hours, but since it'll just be comments, it won't affect any behavior. @matsbla: if you're around today to test #33, it would be great to get your feedback on it!

matsbla’s picture

I made a quick check now and to me it looks like patch in #33 effectively solve this issue.

Just one thing that I think is a little bit strange. but maybe this is by design;
1. I have unchecked the box "Hide non translatable fields on translation forms"
2. I publish a node with image
3. start make a draft in a second language
4. update and publish a new image in the original language
5. publish the draft of the second language (with the old image)

By publishing that draft I now revert changes made in step 4. It means after a new image is published the same image must be uploaded to every pending revision / draft that was made before, if not publishing each of them will revert the new image.
In one way it makes sense as this mode allow all languages to modify those non-translatable properties. But in another way it would also make sense if new images that were published where synchronized directly to all pending revisions.

In the other mode, when you have that checkbox checked, the new image is persevered, but you only see it once you publish the draft. When you edit the draft or look at the preview, you see the old image. You see the new image first when you publish the draft. What if I were suppose to update the image text? It could have made sense to see the new image there before you publish the pending revision.

Anyway these are just some observations, and I think this patch solve the most important issues.
Great work!

wim leers’s picture

@plach in #29: MUCH clearer!

@matsbla in #35: thanks for confirming that this solved the problem! I don't know how this is meant to work, so I can't add nuance to the new observation you posted. Thank you very much for your continued diligence! :) 👌

plach’s picture

StatusFileSize
new7.37 KB
new46.42 KB

This should address the first issue reported in #35, which was indeed a bug, thanks!

The preview issues also need to be solved but for this issue the focus is on fixing the data-integrity problems, so this will need to be addressed in a follow-up. The proper solution (also to the issue above) will likely be to introduce a new hook_entity_revision_create hook exploiting which CT would mimic the merge logic we have for untranslatable fields. This would allow to display the form with all the correct values already, instead of "making things right" on submit.

plach’s picture

@matsbla:

Of course a confirmation that the issue is fixed and that the patch is finally preventing any kind of unintended revert would be great :)

matsbla’s picture

Looks like it is working fine now, nice!

effulgentsia’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs change record

I would still like to improve/add some code comments per #34, which I didn't get to yesterday due to looking more into #35 (thanks again for that great find, @matsbla) with @plach. However, I woke up today not feeling very well, so am not yet sure if I'll be able to work on those docs/comments today. Meanwhile, beta is approaching, and getting this,
#2860097: Ensure that content translations can be moderated independently, and #2936511: ContentEntityType::getRevisionMetadataKeys() never considers the BC logic because of the new "revision_default" key in by then would be super awesome, whereas docs improvements can happen after beta if necessary. Therefore, since I'm happy with all of the code itself in #37, to keep things moving, RTBC!

+++ b/core/modules/content_translation/src/FieldTranslationSynchronizerInterface.php
@@ -54,4 +55,15 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
+  public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition);

This needs a change record.

plach’s picture

Issue tags: -Needs change record

Thanks, here is the CR: https://www.drupal.org/node/2941474

  • Gábor Hojtsy committed bbfa34e on 8.6.x
    Issue #2940204 by plach, Wim Leers, effulgentsia, matsbla: Translatable...

  • Gábor Hojtsy committed fb3c36a on 8.5.x
    Issue #2940204 by plach, Wim Leers, effulgentsia, matsbla: Translatable...
gábor hojtsy’s picture

Version: 8.6.x-dev » 8.5.x-dev
Status: Reviewed & tested by the community » Fixed

Looks good to me too, committed to both 8.6 and 8.5. There was this little code style problems that I fixed on commit:

FILE: ...modules/content_translation_test/content_translation_test.module
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 33 | ERROR | [x] Concat operator must be surrounded by a single
    |       |     space
----------------------------------------------------------------------

Publishing change record.

tacituseu’s picture

Status: Fixed » Needs work

This is failing on 8.5.x:
https://www.drupal.org/pift-ci-job/878041
https://www.drupal.org/pift-ci-job/878043
https://www.drupal.org/pift-ci-job/878038

Drupal\Tests\content_translation\Kernel\ContentTranslationFieldSyncRevisionTest::testFieldSynchronizationAndValidation
The following module is missing from the file system: simpletest

/var/www/html/vendor/symfony/phpunit-bridge/DeprecationErrorHandler.php:270
/var/www/html/core/includes/bootstrap.inc:268
/var/www/html/core/includes/bootstrap.inc:291
/var/www/html/core/tests/Drupal/Tests/TestFileCreationTrait.php:71
/var/www/html/core/modules/content_translation/tests/src/Kernel/ContentTranslationFieldSyncRevisionTest.php:100

  • plach committed 59eefa0 on 8.6.x
    Revert "Issue #2940204 by plach, Wim Leers, effulgentsia, matsbla:...

  • plach committed cfbe3fc on 8.5.x
    Revert "Issue #2940204 by plach, Wim Leers, effulgentsia, matsbla:...
plach’s picture

Status: Needs work » Needs review
StatusFileSize
new846 bytes
new46.43 KB

Reverted due to the failures above.

The test was failing on 8.5.x because of TestFileCreationTrait, that relies on some test files provided by the simpletest module. It seems that 8.6.x is able to automatically track this dependency while 8.5.x isn’t. The attached patch fixes the issue in my local environment, let's see whether testbots agree.

plach’s picture

Version: 8.5.x-dev » 8.6.x-dev
wim leers’s picture

Status: Needs review » Reviewed & tested by the community

@tacituseu: thanks for your diligence!

@plach's explanation in #48 makes sense.

ContentTranslationFieldSyncRevisionTest() passed on 8.5.x, moving back to RTBC!

  • Gábor Hojtsy committed 7e343df on 8.6.x
    Issue #2940204 by plach, Wim Leers, effulgentsia, matsbla, Gábor Hojtsy...

  • Gábor Hojtsy committed bbb4222 on 8.5.x
    Issue #2940204 by plach, Wim Leers, effulgentsia, matsbla, Gábor Hojtsy...
gábor hojtsy’s picture

Version: 8.6.x-dev » 8.5.x-dev
Status: Reviewed & tested by the community » Fixed

Thanks everyone, committed again. We should really make the extension discovery behavior harmonized between the two branches because this will cause problems. This is the second such issue since we opened 8.6.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

sam152’s picture

Some users are reporting that this validator is causing exceptions when changing the default language of an entity. Issue here: #2995655: Invalid translation language (und) specified. in Drupal\Core\Entity\ContentEntityBase when changing language of an entity.