Problem/Motivation

In several related issue it was brought up that we are adding methods dealing with revision translation support in ContentEntityInterface and ContentEntityStorageInterface. This is not consistent with the interface composition approach we have taken so far. For instance all methods providing revision support now live in RevisionableInterface and RevisionableStorageInterface.

Additionally the documentation around revision translation is incomplete and needs some clean-up.

Proposed resolution

  • Add \Drupal\Core\Entity\TranslatableInterface and the corresponding TranslatableStorageInterface and move all relevant methods there.
  • Add TranslatableRevisionableInterface and the corresponding TranslatableRevisionableStorageInterface and move all the relevant methods there. Make ContentEntityInterface and ContentEntityStorageInterface extend those.
  • Update and complete the related docs accordingly.

Remaining tasks

  • Validate the proposed solution
  • Write a patch
  • Perform reviews

User interface changes

None

API changes

Only BC additions.

Data model changes

None

CommentFileSizeAuthor
#50 entity-revision_translation_api-2929496-50.interdiff.txt720 bytesplach
#50 entity-revision_translation_api-2929496-50.patch17.96 KBplach
#49 entity-revision_translation_api-2929496-49.interdiff.txt3.18 KBplach
#49 entity-revision_translation_api-2929496-49.patch17.96 KBplach
#47 entity-revision_translation_api-2929496-47.interdiff.txt3.16 KBplach
#47 entity-revision_translation_api-2929496-47.patch19.77 KBplach
#35 entity-revision_translation_api-2929496-35.interdiff.txt1.81 KBplach
#35 entity-revision_translation_api-2929496-35.patch19.77 KBplach
#35 entity-revision_translation_api-2929496-35.interdiff.txt1.81 KBplach
#35 entity-revision_translation_api-2929496-35.patch19.77 KBplach
#33 entity-revision_translation_api-2929496-33.interdiff.txt1.67 KBplach
#33 entity-revision_translation_api-2929496-33.patch19.62 KBplach
#27 entity-revision_translation_api-2929496-27.interdiff.txt1.97 KBplach
#27 entity-revision_translation_api-2929496-27.patch19.59 KBplach
#22 entity-revision_translation_api-2929496-22.interdiff.txt3.44 KBplach
#22 entity-revision_translation_api-2929496-22.patch19.4 KBplach
#15 entity-revision_translation_api-2929496-14.interdiff.txt5.18 KBplach
#15 entity-revision_translation_api-2929496-14.patch19.25 KBplach
#13 entity-revision_translation_api-2929496-13.interdiff.txt1.79 KBplach
#13 entity-revision_translation_api-2929496-13.patch15.85 KBplach
#10 entity-revision_translation_api-2929496-10.interdiff.txt7.65 KBplach
#10 entity-revision_translation_api-2929496-10.patch15.75 KBplach
#8 entity-revision_translation_api-2929496-8.interdiff.txt884 bytesplach
#8 entity-revision_translation_api-2929496-8.patch11.08 KBplach
#5 entity-revision_translation_api-2929496-4.interdiff.txt4.55 KBplach
#4 entity-revision_translation_api-2929496-4.interdiff.txt3.28 KBplach
#4 entity-revision_translation_api-2929496-4.patch11.17 KBplach
#2 entity-revision_translation_api-2929496-2.patch6.62 KBplach

Comments

plach created an issue. See original summary.

plach’s picture

Assigned: plach » Unassigned
Status: Active » Needs review
StatusFileSize
new6.62 KB

Here's an initial patch, I'll work on the docs next, if none beats me to it.

plach’s picture

Status: Needs review » Needs work
plach’s picture

Status: Needs work » Needs review
Related issues: +#2892377: Document relationship of entities, entity revisions and entity translations
StatusFileSize
new11.17 KB
new3.28 KB

Here is the additional documentation. This moves the existing section before any CRUD operation description, since it's a general overview.

plach’s picture

StatusFileSize
new4.55 KB

Wrong interdiff, sorry.

plach’s picture

timmillwood’s picture

A couple of small points, otherwise looks good.

  1. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,47 @@
    + * Only changes to the default revision can be performed without triggering the
    + * creation of a new revision, in any other case revision data is not supposed
    + * to change. Aside from historical data, there can be "pending" revisions, that
    

    I'm not sure this sentence reads well. You could make changes to a default revision and create a new revision, but only if the new revision becomes the default. You could also make a change to a pending revision without creating a new revision.

  2. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -245,6 +279,7 @@
    +
    

    Not sure we need this new line.

plach’s picture

StatusFileSize
new11.08 KB
new884 bytes

Discussed wording at length with Tim and Nat in Slack. We decided to just replace "can" with "may" :D

The interdiff does not show it but also the the empty new line is gone.

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

+1

plach’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new15.75 KB
new7.65 KB

I realized that we were missing a few interfaces to complete the symmetry between translatable and revisionable entity types.

plach’s picture

hchonov’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,48 @@
    + * approval, before being accepted as canonical. See
    + * \Drupal\Core\Entity\RevisionableInterface and
    + * \Drupal\Core\Entity\RevisionableStorageInterface for more details.
    ...
    + * \Drupal\Core\Entity\TranslatableInterface and
    + * \Drupal\Core\Entity\TranslatableStorageInterface for more details.
    ...
    + * \Drupal\Core\Entity\TranslatableRevisionableInterface and
    + * \Drupal\Core\Entity\TranslatableRevisionableStorageInterface for more
    + * details.
    

    Not important, but probably we could exchange the sentence by simply using @see tag to both interfaces.

  2. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,48 @@
    + * for each enabled language. Some fields may be defined as untranslatable, that
    + * is shared among all translations. The "default" translation is the canonical
    

    the first part of the sentence reference to "fields" in plural, but the second one is using a singular form, which is somehow confusing.

  3. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,48 @@
    + * version of the entity, the one whose content will be accessible in the entity
    + * field data, if no language is specified. Other translation objects can be
    

    I think it is worth also mentioning that the default translations is the translation in which the entity has been initially created before it has been translated.

  4. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,48 @@
    + * instantiated from the default one. A translation is identified by the entity
    

    We can instantiate translation objects from any entity translation object not only from the default one :).

  5. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,48 @@
    + * identifier and the "active language". See
    

    What does mean that a translation is identified by something? I think this sentence isn't needed.

  6. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,48 @@
    + * canonical version of the entity is the default translation of the default
    

    Hmm I would say that the canonical version or actually any version is loaded initially in the default translation.

  7. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,48 @@
    + * is considered "affected" by that revision. With the built-in UI a translation
    + * change will create a new revision (with a copy of all field data for other
    + * translations), unless the default revision is being modified (no new revision
    

    Oh, I wasn't aware of this. Where are we doing this? :)

plach’s picture

StatusFileSize
new15.85 KB
new1.79 KB

1: Not sure what you mean :)
2: Mmh, "that is" is equivalent to "i.e." in this case. I think it's correct English but it would be good to have confirmation from a native speaker.
3: Given that we support changing the default language, the values may be the same of the initial ones, but technically the translation would not be the same in that case. It's a tricky area I'd like to leave out, given that it doesn't add much to what we already have IMO.
4: Sure, but we always start from the default one. Again, I'm not sure it's worth complicating this description, given that this information is self-evident, once you realize all translations share the same class.
5: I was just trying to introduce the concept of "active language", which is a key bit. Maybe now it's better?
6: Fixed
7: Well, if you load the entity with all field translation values, and you change them only in one language, when you save a new revision all field translations are stored, including the unchanged ones, so you get a copy for all untouched languages. It's the default entity storage behavior. Maybe the description is confusing?

The last submitted patch, 10: entity-revision_translation_api-2929496-10.patch, failed testing. View results

plach’s picture

StatusFileSize
new19.25 KB
new5.18 KB

This should hopefully fix all test failures.

plach’s picture

Issue summary: View changes

Updated the IS with the description of the API changes.

plach’s picture

Issue summary: View changes
plach’s picture

Issue summary: View changes

Minor clean-up

hchonov’s picture

1. I mean

+ * @see \Drupal\Core\Entity\RevisionableInterface
+ * @see \Drupal\Core\Entity\RevisionableStorageInterface

instead of

+ * See
+ * \Drupal\Core\Entity\RevisionableInterface and
+ * \Drupal\Core\Entity\RevisionableStorageInterface for more details.

2. ok :).

3. Ok, I agree.

4. Ok, I agree.

5. I think that it is better now. Thank you.

6. Thank you.

7. Hmm I am not sure, but there is something I don't really understand. Sorry about this.
By reading

+ * With the built-in UI a translation
+ * change will create a new revision (with a copy of all field data for other
+ * translations), unless the default revision is being modified (no new revision
+ * is created).

I don't get it how a non-default revision could be edited (we don't have a built-in UI for this)?

clairedesbois@gmail.com’s picture

@hchonov For the point 7: You can modify the last revision (which isn't the default revision) with content moderation. But yes, the sentence is a little confuse, it give the impress that we can translate (understand initialize the translation) with a non-default revision. I don't think it's the case even with content moderation

(after verification, yes, content moderation create a new translation from the default revision of the source language which is the published revision if the content have pending revisions)

plach’s picture

That sentence is trying to the convey the idea that from the built-in UI only the default revision can be modified without triggering the creation of a new revision (this is not supported by Content Moderation, which always triggers a new revision on save). Hence that's the only case when you can get a revision that affects multiple translations (at least via the UI).

plach’s picture

StatusFileSize
new19.4 KB
new3.44 KB

I hope this reads better.

plach’s picture

Issue summary: View changes

More IS clean-up

timmillwood’s picture

Looks good to me, but I think @hchonov should RTBC based on his recent reviews.

hchonov’s picture

+++ b/core/lib/Drupal/Core/Entity/entity.api.php
@@ -68,6 +68,51 @@
+ * field values for the other translations will be copied as-is. However, if the
+ * default revision is being modified without creating a new revision, multiple
+ * translations can be changed. In this case, multiple translations may be

I've needed some time until I've realized what this exactly means...

So the idea here is that we could subsequently edit the default entity revision without saving a new revision while each edit is performed on a different entity translation. In this case the revision translation affected flag will be set for each of the edited entity translations in the default entity revision.

I think it would be much better to understand for others if this is somehow mentioned here.

I would suggest something like this:
However, if the default revision is being modified subsequently for different entity translations without creating a new revision when saving each the translations, multiple translations can be changed and will be flagged as affected.

hchonov’s picture

Oh and I think we have to document that editing untranslatable fields results into "affecting" all the entity translations.

plach’s picture

Issue summary: View changes
StatusFileSize
new19.59 KB
new1.97 KB

This should address the comments above.

hchonov’s picture

Status: Needs review » Reviewed & tested by the community

Thank you. It looks much better now :).

amateescu’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
    @@ -8,7 +8,7 @@
    -use Drupal\Core\TypedData\TranslatableInterface;
    +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
    
    @@ -253,7 +253,7 @@ public function buildMultiple(array $entities) {
    -          if ($entity instanceof TranslatableInterface && $entity->isTranslatable()) {
    +          if ($entity instanceof TranslatableDataInterface && $entity->isTranslatable()) {
    
    +++ b/core/lib/Drupal/Core/Entity/EntityRepository.php
    @@ -5,7 +5,7 @@
    -use Drupal\Core\TypedData\TranslatableInterface;
    +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
    
    @@ -82,7 +82,7 @@ public function loadEntityByConfigTarget($entity_type_id, $target) {
    -    if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
    +    if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) {
    
    +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
    @@ -11,7 +11,7 @@
    -use Drupal\Core\TypedData\TranslatableInterface;
    +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
    
    @@ -189,7 +189,7 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode) {
    -      if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
    +      if ($entity instanceof TranslatableDataInterface && count($entity->getTranslationLanguages()) > 1) {
    

    Is there any reason for not using the new interface?

    We are in the Drupal\Core\Entity namespace so it's not like we're expecting typed data objects here..

  2. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,54 @@
    + * for each enabled language. Some fields may be defined as untranslatable, that
    + * is shared among all translations. The "default" translation is the canonical
    

    Small nitpick here: Some fields ... that are shared ...

    However, there's something about how this sentence is connected which doesn't sound right. How about something like this:

    "Some fields may be defined as untranslatable, which means that their values are shared among all translations."

andypost’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
@@ -20,70 +18,7 @@
+interface ContentEntityInterface extends \Traversable, FieldableEntityInterface, TranslatableRevisionableInterface {

+++ b/core/lib/Drupal/Core/Entity/TranslatableRevisionableStorageInterface.php
@@ -0,0 +1,9 @@
+ * A storage that supports translatable and revisionable entity types.
...
+interface TranslatableRevisionableStorageInterface extends TranslatableStorageInterface, RevisionableStorageInterface {
+}

Is there any reason for empty interfaces?

And why all content defined revisionable?

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 27: entity-revision_translation_api-2929496-27.patch, failed testing. View results

timmillwood’s picture

Status: Needs work » Reviewed & tested by the community

DrupalCI fail, so back to RTBC.

plach’s picture

StatusFileSize
new19.62 KB
new1.67 KB

@amateescu:

1: Strictly speaking, replacing \Drupal\Core\TypeData\TranslatableInterface with \Drupal\Core\Entity\TranslatableInterface in those instanceof checks is a BC break, because we are narrowing the set of supported implementers. In practice, I agree it's very unlikely anything will change, as we will always be dealing with content entities. However it's still an unneeded BC break: I'd avoid pissing the release managers with those, given that we are already proposing one :)

2: It was meant to be equivalent to "i.e.", but your suggestion looks better anyway.

@andypost:

That's a placeholder for the method introduced in #2926483: Add API methods for determining whether an entity object is the latest (translation-affecting) revision.

All content entities have always supported revisions at API level, since the related methods belong to ContentEntityInterface / ContentEntityStorageInterface prior to this patch, we are just making it more explicit. Actually the definition of a content entity is "a fieldable, revisionable, and translatable" entity type, at least at the moment.

catch’s picture

Couple of minor questions, leaving RTBC for now:

  1. +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
    @@ -8,7 +8,7 @@
     use Drupal\Core\Entity\EntityDisplayBase;
    -use Drupal\Core\TypedData\TranslatableInterface;
    +use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
     
    

    Is this because of actual conflicts or just clarity? If the latter shouldn't the namespace be enough?

  2. +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
    @@ -253,7 +253,7 @@ public function buildMultiple(array $entities) {
               // - the current "content language" otherwise.
    -          if ($entity instanceof TranslatableInterface && $entity->isTranslatable()) {
    +          if ($entity instanceof TranslatableDataInterface && $entity->isTranslatable()) {
                 $view_langcode = $entity->language()->getId();
    

    Is this just for clarity or is there an actual conflict?

    Also wondering if there are any other implications to entities being able to implement two things called translatable interface.

  3. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,55 @@
    + * the entity, the one that is loaded when no specific revision is requested.
    

    On the one hand this says 'can keep', on the other it says the full history is stored.

    Not sure if we should assume that revisions are being kept (in which case change 'can keep' to 'keeps'?) or add the caveats elsewhere.

  4. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,55 @@
    + * for each enabled language. Some fields may be defined as untranslatable,
    + * which means that their values are shared among all translations. The
    + * "default" translation is the canonical version of the entity, the one whose
    + * content will be accessible in the entity field data, if no language is
    + * specified. Other translation objects can be instantiated from the default
    

    Is it worth pointing out that the 'default translation' is usually the original language the entity was added in - i.e. sometimes the source language rather than a translation?

plach’s picture

@catch:

1: There was an actual conflict with EntityViewBuilder: since we have a TranslatableInterface in \Drupal\Core\Entity, i.e. the same namespace of EVB, it was not possible to use another interface with a conflicting name (see #10). For consistency, I decided to alias the other two occurrences.
2: Well, the difference will be that code using the type data version in type hints won't get the ::hasTranslationChanges() method. This is already the case when preferring TranslatableInterface over ContentEntityInterface. I don't think there will be other issues because conflicts can only happen in the \Drupal\Core\Entity namespace, which core owns.
3: Fixed
4: Fixed. This was brought up also by @hchonov in #12.3: I tried to avoid describing the language change scenario (see #13.3) by adding "typically".

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 35: entity-revision_translation_api-2929496-35.patch, failed testing. View results

plach’s picture

Status: Needs work » Reviewed & tested by the community

Testbot issue

effulgentsia’s picture

I think this patch looks great. Regarding the BC break:

+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php
@@ -5,24 +5,7 @@
-   * @return \Drupal\Core\Entity\ContentEntityInterface
-  public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []);
...
+++ b/core/lib/Drupal/Core/Entity/TranslatableStorageInterface.php
@@ -0,0 +1,27 @@
+   * @return \Drupal\Core\Entity\TranslatableInterface
+  public function createTranslation(TranslatableInterface $entity, $langcode, array $values = []);

The issue summary and change record identify this as a potential API change, but I think are incorrect about what the BC break actually is. I don't think that the change of return value type is a BC break at all, because all current callers pass a ContentEntityInterface object (per HEAD's API) and therefore even after the patch, they'll continue to get back a ContentEntityInterface object.

However, the change to the parameter typehint is a BC break for all classes that override this method. The CR says "Implementers extending ContentEntityStorageBase have nothing to do", but that's only true if they don't override this method. If they do extend the base class and override the method, then they do have something to do: they must change the typehint in the overridden method. https://www.drupal.org/core/d8-bc-policy#interfaces is currently unclear as to whether such a change is allowed.

Obtain release manager signoff

Given the above BC break and the lack of clarity in the current BC policy, I agree that release manager signoff is needed. Tagging it for that.

plach’s picture

Issue summary: View changes

Thanks, @effulgentsia!

The issue summary and change record identify this as a potential API change, but I think are incorrect about what the BC break actually is. I don't think that the change of return value type is a BC break at all, because all current callers pass a ContentEntityInterface object (per HEAD's API) and therefore even after the patch, they'll continue to get back a ContentEntityInterface object.

The point is that if your code strictly relies on a ContentEntityInterface being returned, once we perform this change it may also get an instance of TranslatableInterface not extending ContentEntityInterface and thus break. OTOH I pointed out in the IS why I think this is an highly unlikely scenario.

However, the change to the parameter typehint is a BC break for all classes that override this method. The CR says "Implementers extending ContentEntityStorageBase have nothing to do", but that's only true if they don't override this method.

Good point! I had mentioned a similar case in the IS but forgot to update the CR to reflect that. Updated both.

plach’s picture

Issue tags: +API change
wim leers’s picture

Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,57 @@
    + * @section entities_revisions_translations Entities, revisions and translations
    + * A content entity can have multiple stored variants: based on its definition,
    + * it can be revisionable, translatable, or both.
    

    😱🎉👏🙏

  2. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,57 @@
    + * historical information. The "default" revision is the canonical version of
    

    "version" here is very confusing.

  3. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,57 @@
    + * to change. Aside from historical data, there can be "pending" revisions, that
    

    s/data/revisions/

  4. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,57 @@
    + * "default" translation is the canonical version of the entity, the one whose
    

    "version" here is NOT confusing.

  5. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,57 @@
    + * content will be accessible in the entity field data. Other translation
    + * objects can be instantiated from the default one. Every translation has an
    

    "translation objects"?

    Aren't those just "entity objects"?

  6. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,57 @@
    + * An entity that is both revisionable and translatable has all the features
    + * described above: every revision can contain one or more translations. The
    

    This one sentence. I'd have begged for this one sentence a long time ago.

    Do translations contain revisions, or revisions contain translations?

    Now it's clear. Thank you!

  7. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,57 @@
    + * "revision_translation_affected" field. With the built-in UI, every time a new
    

    Glad to see this documented clearly as well now. Very important for the API-First Initiative, when it starts to support revisions and translations!

  8. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -68,6 +68,57 @@
    + * untranslatable fields. On the other hand, pending revisions are not supposed
    + * to contain multiple affected translations, even when they are being
    + * manipulated via the API.
    

    Pending revisions aren't supposed to affect multiple translations.

    That means it's impossible to translate a new revision in all translations ahead of time, and keep it all in sync?

  9. +++ b/core/lib/Drupal/Core/Entity/entity.api.php
    @@ -113,17 +168,6 @@
    - * A translation is not a revision and a revision is not necessarily a
    - * translation. Revisions and translations are the two axes on the "spreadsheet"
    - * of an entity. If you use the built-in UI and have revisions enabled, then a
    

    This is what I added a while ago based on Gábor's insight. The docs you're adding are 10x more detailed and therefore 10x more useful :)

catch’s picture

That means it's impossible to translate a new revision in all translations ahead of time, and keep it all in sync?

My understanding is that it means a single pending revision should only make changes to one translation. However a second pending revision could then be created with changes to a different translation. Subsequently when that second revision is saved again as the default, the default will have changes to both translations - but until that moment only one translation has been 'affected' at a time.

I feel like the the createTranslation change isn't something we can get away with - we allow interface additions because it's guaranteed you don't need to do anything when using the base class (except for the usually theoretical possibility of already declaring a method with the same name), but changing a method that subclasses might have implemented seems a lot more likely. If we don't allow it, then we need to figure out how to handle it though (add another method?).

effulgentsia’s picture

If we don't allow it, then we need to figure out how to handle it though (add another method?)

How about TranslatableInterface::createNewTranslation()? It's a bit redundant, since "create" implies "new", but I don't have a better idea at the moment. While we're on the topic, are we sure that it's always "new"? The docs say it is ("Constructs a new entity translation object"), but ContentEntityStorageBase::createTranslation() creates it by calling $translation = $entity->getTranslation($langcode);, and looking at that code path, I'm not entirely clear if there are cases where an existing translation gets reused.

plach’s picture

@catch:

My understanding is that it means a single pending revision should only make changes to one translation. However a second pending revision could then be created with changes to a different translation. Subsequently when that second revision is saved again as the default, the default will have changes to both translations - but until that moment only one translation has been 'affected' at a time.

Actually the ::createRevision() method introduced in #2924724: Add an API to create a new revision correctly handling multilingual pending revisions merges the specified entity translation object with all the other translations available in the default revision. This ensures you can create a pending revision for each language and eventually save them as default revisions without affecting the other languages. But, yes, the key concept is that you can work on translations independently, as long as every pending revision deals with a single translation.

I feel like the the createTranslation change isn't something we can get away with - we allow interface additions because it's guaranteed you don't need to do anything when using the base class (except for the usually theoretical possibility of already declaring a method with the same name), but changing a method that subclasses might have implemented seems a lot more likely. If we don't allow it, then we need to figure out how to handle it though (add another method?).

In general I'd agree with this line of thinking. However, In this specific case I felt bullish about proposing this change because ContentEntityStorageBase::createTranslation() has exactly one usage in core: it's invoked by ContentEntityBase::addTranslation() and has no explicit test coverage because all actual usages go through the latter. I'm pretty confident this is the same for the contrib and custom spaces, but I can do some research if needed.

If this is still deemed not viable I'm happy with the proposed workaround of adding a new method with a different name and leave the old one in place as a wrapper.

plach’s picture

This is what a rather comprehensive (more than 4600 modules) grep on the contrib space yields:

http://grep.xnddx.ru/search?text=createTranslation%28&filename=

No usage at all :)

plach’s picture

This just addresses #41, I'm waiting for feedback on #44 and #45 before getting rid of the API change.

@effulgentsia, #43:

::createTranslation() supports existing translations to be able to add a new translation, even if it was previously removed without saving the entity in between (i.e. restore it), so I think ::createNewTranslation() still respects the intended usage.

However, I think I slightly changed my mind about introducing a new method right now. The goal of this issue is cleaning-up the revision translation API (and the related docs). If we add a slightly misnamed (or inconsistent) method just to preserve BC, we are not really cleaning up things, instead we are adding cruft we won't be able to get rid of in D9, because of the continuous upgrade policy.

TBH this is the main doubt I have around continuous upgrade: it doesn't let us clean up things properly because in many cases the good names will be already taken by the stuff we are deprecating, which will result in a less than optimal DX. I've been thinking about this for a while, and I don't have a good solution to suggest at the moment. Probably it would be a good idea to open a policy issue to discuss this topic a bit more and figure out how this is being handled in other projects or whether there are consolidated good practices to handle such cases.

plach’s picture

StatusFileSize
new19.77 KB
new3.16 KB

The patch for reals

wim leers’s picture

#47 addresses my docs clarity concerns that I raised in #41.

Not RTBC'ing simply because the discussion I spawned with #41.8 doesn't seem to have reached a conclusion yet. I personally don't think that needs to be clarified/solved in this issue. This issue already presents a massive leap forward. I also feel less qualified to decide whether that should be in-scope or not.

As far as I'm concerned: RTBC again.

plach’s picture

Issue summary: View changes
Issue tags: -Needs release manager review, -API change
StatusFileSize
new17.96 KB
new3.18 KB

Discussed this with @catch: we agreed to split the API change to a follow-up issue to be discussed separately. Here is an updated patch.

This is the follow-up: #2932049: Change TranslatableStorageInterface::createTranslation() to accept TranslatableInterface.

plach’s picture

StatusFileSize
new17.96 KB
new720 bytes

Forgot a bit

The last submitted patch, 49: entity-revision_translation_api-2929496-49.patch, failed testing. View results

catch’s picture

Looks good to me without that change, we can figure out how/if to do it in the follow-up.

Status: Needs review » Needs work

The last submitted patch, 50: entity-revision_translation_api-2929496-50.patch, failed testing. View results

plach’s picture

Status: Needs work » Needs review

Testbot-- :(

plach’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC per #48 and #52.

effulgentsia’s picture

Thanks! Adding credit to reviewers.

  • effulgentsia committed 8e9f1b6 on 8.5.x
    Issue #2929496 by plach, hchonov, timmillwood, catch, Wim Leers,...

The last submitted patch, 47: entity-revision_translation_api-2929496-47.patch, failed testing. View results

effulgentsia’s picture

Status: Reviewed & tested by the community » Fixed
+++ b/core/lib/Drupal/Core/Entity/entity.api.php
@@ -68,6 +68,57 @@
+ * A content entity can have multiple stored variants: based on its definition,

I have some reservations about the word "variant". But I'm not able to think of a better word to use at the moment. In any case, any further docs improvements could easily be done in a followup.

Therefore, pushed to 8.5.x!

plach’s picture

Thanks! Published the CR.

Status: Fixed » Closed (fixed)

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