Problem/Motivation
From @Berdir #2942914.14:
- If content moderation is enabled and the entity has a pending draft in the given language, always use that as the source.
- We use \Drupal\tmgmt\SourcePluginUiBase::reviewForm() and the related validate/submit methods to show an element above the accept buttons that allows to...
- If content moderation is enabled, allows to select the desired workflow state (draft/published/...) that it will have. While we have those methods, there isn't really an official way to store this information, so my proposal would be to introduce a special key like #workflow_state or so as top-level key in the data array of the job item, then we check for that and do the necessary steps when accepting the translation.
- As a fallback, if content_moderation is not enabled, we just show a Published yes/no checkbox if the entity type implements EntityPublishedInterface(), default to published unless the source is still unpublished as well.
And of course extensive test coverage for that. Entities with and without content moderation, with pending revisions on the selected source language, entities that already have translations in the target language, both as published as well as draft.
Proposed resolution
Remaining tasks
User interface changes
API changes
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #65 | 2978341-65-64-interdiff.txt | 5.76 KB | mbovan |
| #65 | 2978341-65.patch | 56.34 KB | mbovan |
| #64 | 2978341-64-63-interdiff.txt | 9.75 KB | mbovan |
| #64 | 2978341-64.patch | 56.23 KB | mbovan |
| #63 | 2978341-63-62-interdiff.txt | 4.65 KB | mbovan |
Comments
Comment #2
mbovan commentedFirst step... This patch always uses the latest revision on the source overview and all the other places where we load entities.
There are multiple places where we load a target entity and use the label. This means that in case there are forward revisions created on the target entity (that affect title/label), we will always use the label of the latest revision. However, it is more a UX problem at this point IMO.
Comment #4
berdirComment #5
berdirWhat you are missing is a check for $entity_type->isRevisioanble.
We also need to respect the language code from the job, not the current language, so the job/langcode is a requirement argument here.
Comment #6
mbovan commentedPatch update that addresses #5 where applicable and fixes the preview mode.
There are places where we can't rely on the job source language when loading entities:
ContentEntitySource::getSourceLangCode()- a job entity does not exist yet when this method is calledContentEntitySource::getExistingLangCodes()- language is not relevantAlso, loading the latest revision is not only being used in
tmgmt_content, so I moved the logic totmgmtcore module.I will start writing tests in the next updates.
Comment #8
mbovan commentedFixing several cases where we don't have
$jobentity available.Also, the correct check whether entities are revisionable seems to be
$entity_type->isRevisionable()rather than$entity instanceof RevisionableInterface.Comment #9
berdirthis should use t(), not sprintf().
not sure if we want to use this service in kernel tests. We also have lots and lots of load calls, those just use $class::load().
Also with the content moderation specific tests, I'd say we want to explicitly load by revision ID, to make sure our service does what it should.
woah, this is weird, looks like we ported that along for some time.
We only care about the label and url, we have generic methods to get that from the source. Lets open a separate issue to clean that up and don't touch it here.
This is content entity specific, so it should be in tmgmt_content, as mentioned above, the only case that is not above is a bug and code that shouldn't exist (and seems mostly unused outside of tests)
I don't think we should catch/support that, with the case above removed, this should never happen.
I'd prefer using NULL for this instead of DEFAULT. Most cases that use NULL then fall back to the current language and while I agree that the original language of the entity does make sense here, I think I prefer NULL :)
In general, this looks good so far, but we're going to need a lot of test coverage, basically for a lot of features we have like basic jobs, continuous, preview and so on.
Comment #10
mbovan commentedThanks for the review!
#9:
entity_loadwhere the entity language is not relevant. Also, I created a follow-up issue to clean and refactor\Drupal\tmgmt\Entity\JobItem::accepted()#2979126: Refactor and cleanup JobItem::accepted(). However, a use case when one is translating a draft revision fails on:$translation->getTranslation($this->getJob()->getTargetLangcode()), so I copied the logic fromContentEntitySource::load()until we fix it properly in #2979126: Refactor and cleanup JobItem::accepted()...ContentEntitySourceSetting up to "Needs review" to check existing tests.
Comment #11
mbovan commentedTest
entity_loadfixes.Comment #12
mbovan commentedProgress update: addressed the point 2 from the issue summary.
This patch adds content moderation element for moderated entities and published checkbox for publishable entities.
The review form for moderated entities:
The publish status form element for publishable (but not moderated) entities:
The auto-accept use-case is not yet supported. As per #14 we will do this in a follow-up.
Comment #14
mbovan commentedThe tests in #12 failed when testing outdated translations as we are now always creating a new revision.
The logic from
\Drupal\tmgmt_content\ContentEntitySourcePluginUi::overviewRow():loads the translation languages on the entity (revision) returned by
\Drupal\Core\Entity\TranslatableRevisionableStorageInterface::getLatestTranslationAffectedRevisionId()which does a fallback to the source entity language.In the test scenario (
\Drupal\tmgmt_content\Tests\ContentEntitySourceUiTest::testNodeTranslateTabSingleCheckout()) there is a mismatch on what\Drupal\Core\Entity\TranslatableRevisionableStorageInterface::getLatestTranslationAffectedRevisionId()(for the source language) returns and the actual latest revision ID... Needs to be investigated.Comment #15
mbovan commentedCovered the following cases in UI tests:
Comment #16
mbovan commentedHide one extra patch.
Comment #18
mbovan commentedAdditional tests and general improvements.
Edit:
This code generates a row in the source overview table.
At this point, we want to show the label of the latest revision of the source entity.
However, we also have
$entity->getTranslationLanguages();call which fails to return all the languages when we are calling it on the latest revision.Not sure if is it the best way, but the workaround is to reload the entity (to get the latest default revision) and check translation languages. This is explained in #14 and the reason why #12 and #15 were failing.
In #12 we were creating a new revision on
$translation, but here we are doing it on the source entity (latest default revision). It seems this is what Core does as well.Not sure why is this needed, but without it, the default revision on the source entity gets overridden when we save a translation...
Comment #19
berdirStarted looking into that test fail/workaround. The change fixes this test fail, but it won't be enough if we start to have more than one language.
If you look at \Drupal\content_translation\Controller\ContentTranslationController::overview(), then you can see that it loads the latest affected revision id for each translation and I think that's actually the right thing to do. If we start mixing it with drafts, then we will have a situations where there is a translation that doesn't exist in either the default revision or the source revision, so we can't rely on $entity->getTranslationLanguages() anymore.
I think we should actually look into displaying an icon in case we detect a pending revision (as well as link to it), but that's definitely a follow-up. We should have test coverage for the situation with saving two translations as a draft and making sure both show up, though.
Will continue to review later.
Comment #20
berdirThe order here is wrong, createTranslation() should be called with $translation, as this is very much about a specific translation.
With that change, I'm down to 3 different fails about the revision log message, and when I move that above the createRevision() call, it passes without the translation affected hack. Maybe this should even be at the end, just before the save in fact.
I think we also need better test coverage for that, specifically, we want to test the following situation:
create a source entity translation, then create two different translation jobs, one for each language, then accept both, then make sure that the final revision contains all translations.
I'd prefer to use two different keys for this, published aka true/false is *not* a moderation state and reading it like this is confusing.
The thing is that this would also be true if we would see the spanish site as $title is contained within $draft_title.
Not easy with the default prefixes that we ave with our translations, but maybe we you could at least add an explicit assertNotext() for the translation as well?
don't quite understand the see thing, is that a link? the job title? not sure if that is very useful, at least the wording is strange.. maybe something like "Created by translation job @label"?
Also, looks like at this point, we're already pretty well prepared to improve the test coverage of the source overview page, to make sure that both the german as well as the spanish translation are shown as translated which I'm pretty sure isn't working yet, as you didn't publish the spanish translation yet.
does this happen because the content is unpublished and this user isn't allowed to view that?
I'd prefer giving him bypass node access or so so we can check it's there and then 403 as anonymous user.
this doesn't do anything, test_dependencies is just to test the testbot to fetch additional modules and require-dev in a composer.json is actually the better approach for that because it works immediately.
Since this is in core, we don't need either.
Comment #21
mbovan commentedAddressed the feedback:
I will continue with addressing the remaining.
Comment #22
mbovan commentedComment #23
mbovan commentedThe patch in #21 failed as the workflow change from #2952307: Move workflow config into standard profile was committed to 8.6.x only.
Here is the update. Addressed:
Comment #24
mbovan commentedCreated a #2982270: Introduce an icon if pending revisions are detected on the source overview page follow-up for icons mentioned in #19.
Comment #25
berdirThe labels are a bit confusing.
This is taken from the widget, but the widget is when you edit an existing thing and just resave it.
We're often creating a new translation, or updating it, from another language, so we have more than one "thing" and need to be more specific.
Maybe "Current source state" and "Translation state"?
Also the @see above is technically wrong, @see is only allowed in docblocks, in inline comments, just use "See .. " without @
Do we have test coverage for this stuff?
I think that would for example mean having an entity created in a different language and having a draft for that in that language.
Still not quite sure about this.
Example scenario:
I have a node with title "First node", then I create a draft of that with title "FOO node".
That gives me this:
I can't quite decide if that's a bug or a feature :)
But lets keep it for now and open a follow-up to discuss if the search should work on the latest, default or all revisions. And add a @todo here for that.
what is it now, spanish or italian? ;)
again.
What about just always creating a workflow with a different ID? Then we're not tied to whether it exists by default or not and we also ensure that it doesn't just work with the default.
Comment #26
berdirForgot to add the screenshot above for the search thing:
Comment #27
mbovan commentedAddressed feedback from #25
Comment #28
mbovan commentedComment #29
berdirmaybe just make that part of the create method now? you could pass in the bundle to the method or so.
weird @throws
Comment #30
mbovan commentedAddressed #30.
Yeah, my CS is always complaining about missing doc @throws tags when
Entity::save()is called. Ignored that here.Comment #31
berdirThat's afaik a new PhpStorm feature that I usually disable :)
Comment #33
johnchqueRebasing patch. :)
Comment #35
johnchqueThis should be good for 8.x-1.6 :)
Comment #37
berdirRerolled.
Comment #39
berdirUpdated the patch.
Comment #40
berdirReroll.
Comment #42
berdirConverted the test to phpunit.
Comment #43
keats76 commented@Berdir
Is there a timeline on this item? We could really use this in our 8.7.1 instance and it would be nice to set expectations.
Thanks for the hard work!
Mike
Comment #44
mbovan commentedRerolled after #3063547: Error on translation when content moderation is enabled.
Comment #45
casaran commentedHi everyone.
We have some similar requirements as the ones in this patch but we need more things. My intent is to extend the patch and build what we miss upon it. I was wondering however if it was a good idea to do it in this issue or to create a separate issue for it. What I am planning to add is the following:
Comment #46
berdirI believe that's not something that you can achieve in the UI, so I'm not sure about supporting that out of the box. Also, content_moderation enforces new revisions every time you save, so I'm not sure what the use cases are for translating specific old revisions?
Comment #47
keats76 commented@Berdir - Use Case: You have draft content that is not ready to publish. An example might be a new product announcement. You need to translate that draft. When the translations come back, you don't want to auto publish them because the product has not yet launched. You want the translated content to exist in the same state as the source. When the time is right, you would publish all.
In my brief testing, the way this patch works is that it allows draft (moderation state) translation, but the returned translations are immediately published.
Comment #48
berdirSee comment #12, there a UI to select the target status/moderation state and it should default to what the source does.
Comment #49
berdirThat should work, but there might be bugs.
What doesn't work is translating a published version while there is a draft/forward revision.
Comment #50
keats76 commentedThat makes sense. I only ran a quick test and must have missed that setting.
Will do some more testing and report back if we run into any issues. Thanks!
Comment #51
keats76 commentedWhat is the expected behavior when the provider is set to "auto accept" translations? Should we update the provider settings form to add an option that auto accepts based on moderation state?
Scenario:
1) Create a basic page (published)
2) Create a French translation (published)
3) Update the source page and save as a draft
4) Initiate translation for the draft state using a provider set to auto-accept translations
5) Return the translation and download the file.
In the above scenario the imported translation was auto published for the French node even though the source was in a draft state.
Comment #52
keats76 commentedOk, looks like it was mentioned in #12 "The auto-accept use-case is not yet supported. As per #14 we will do this in a follow-up."
Is anyone working on this item?
Comment #53
ghost of drupal pastI tried to apply the patch against 1.10 and failed. Here. Not hiding the previous one -- it might be good against dev, not sure.
Comment #54
t2k commentedPatch at #53 is correct over 1.x-dev#c90a895a20fc2e52bf74fab60ec1b7af8e9aac3
I made my own from #44 and I have the same result.
Now I will proceed to some test about translation with content moderation
Comment #55
Heisen-blueI have just applied the patch from #53 on 1.10, it is working well :)
Also, I have exactly the same scenario as @keats76 on #51. Is anyone working on this or is it maybe planned?
This feature is really needed by our contributors, I have some time to work on it. Maybe @mbovan or @berdir can help defining a to-do list/checklist in order to provide this feature the right way?
Thanks
Comment #56
berdirSince this is quite a big patch already I think automated workflows should be a follow-up.
The reason this got stuck is that has been implemented in sync with content moderation support in paragraphs, but it is assuming an earlier implementation. Basically, it is trying to do #3004099: Allow to translate paragraphs from pending revisions but that doesn't work and I don't know how to solve that without changing core.
Specifically, if you add a new paragraph in a new draft, then translating that in the UI will still be based on the default revision, but the implementation in TMGMT will not, and as a result, it will conflict when trying to accept the translation. I'm not sure what will happen exactly, but it won't be pretty.
So to make it possible to commit this, we probably need to do something similar as the paragraph UI, only support paragraphs that exist in the default revision. And we need tests for that, probably best starting with a failing test for such a scenario,
Comment #57
Heisen-blueThx @berdir for your answer.
In my case, I am not using the Paragraphs module. If I understand well, this removes few difficulties.
Can you help me defining the different steps I should follow please? I will create a new issue as you have suggested.
Comment #58
liber_tThis patch doesn't work if use Providers (ex: memsource).
We don't use review interface
Step to reproduce
1) Create Node with draft state in EN
2) Add translation FR (draft status) and push translation to memsource service
3) Pull translation in job page
4) this node in FR have a published status.
Comment #59
berdirIt's not about which provider but having auto-accept enabled or not, as written above, that is not supported yet and would be better done in a separate issue.
Comment #60
colorfieldThank you for the great work on this issue!
I did some manual testing and both cases are working well (with and without content moderation enabled).
Probably not the scope of this issue but adding some thoughts about the translation state to be selected during the Job review:

The next translation state is fetched from the source and not the translation.
Would it make sense to use the next translation state instead of the source there and also display the current one in this case?
E.g. like this, or perhaps remove the current source state then?
This applies only for existing translations though, newly created translations might inherit the state from the source.
Thinking about several translations (e.g. with Trados)
- The source is in the 'published' state
- The translations are in 'draft' state that can then e.g. transition to, say, 'draft' or 'for approval' state
So it might allow to keep restrictions to the transitions then (e.g. not being able to transition from 'published' to 'for approval').
Then it still requires to check how it behaves with auto-accept expectations (#51).
Also, I left some notes here related to the integration with the Xliff File provider if we want to include the latest revision translation
https://www.drupal.org/project/tmgmt/issues/3130344#comment-13571671
Comment #61
colorfieldAfter some more testing with various state transitions it looks good so RTBC+1
Not sure that we want to tackle this here, but spotted an issue related to asymmetric translations (while using this setup https://www.drupal.org/project/paragraphs_asymmetric_translation_widgets).
#3134922: Integration with paragraphs asymmetric translation
Comment #62
colorfieldAbout the moderation state fallback to the published status. On
ContentEntitySourcePluginUi::buildPublishStateElement()we fetch the source published status to set it by default to the translation. Then, we allow to change it, which is fine if the status is translatable.In some cases, it could be synced (e.g. MenuLinkContent "enabled", set as synced by the site builder for other non moderated entity types, ...).
We should probably add the "all languages" hint there if necessary by checking the field definition, to prevent unexpected changes to the published status.

Comment #63
mbovan commentedThis patch goes forward to resolve #56 and prevents saving though the TMGMT user interface if you are trying to save a translation of the pending revision with entity reference revisions field in it.
Basically, if you have a draft Node in English with a Paragraphs field and want to translate it, it's not possible to complete the TMGMT translation before publishing the draft revision of the English/default translation first.
This still needs tests.
Comment #64
mbovan commentedUpdates:
Comment #65
mbovan commentedMore updates:
\Drupal\tmgmt_content\Plugin\tmgmt\Source\ContentEntitySource::saveTranslationfor two reasons: 1) it will prevent auto-accepted translators to mess up the content as mentioned in #12; 2) it allows users to actually translate the content (in the TMGMT UI), validate and save the translation data but it doesn't allow accepting the translation.Comment #66
berdirnot sure if we need to check ER here as well, it wouldn't actually support the composite stuff, but if we do it like that already elsewhere then fine.
Comment #67
mbovan commentedYes, in the other parts of TMGMT we check for both entity reference and entity reference revisions types...
Also, our tests have a simple version of Paragraphs-like entity (
Drupal\tmgmt_composite_test\Entity\EntityTestComposite) that uses entity reference field instead.Comment #69
berdirAwesome, very happy to finally being able to commit this as an acceptable solution. For more advanced use cases that were discussed, please create separate issues, there's also the parent issue still open.