Проблема описана вот тут, и там даже предлагается патч
https://www.drupal.org/files/issues/2024-05-28/3134922-40.patch,
который частично решает проблему, но в #43 siavash (Sia) более подробно рассказывает о том, как сохраняются данные, и становится ясно, что проблема не решена.
По идее, патч должен затрагивать как tmgmt модуль, так и paragraphs_asymmetric_translation_widgets. А так же надо пофиксить уже не верно созданные данные в базе.
Но существующий патч лишь косметически решает проблему до какого то момента. Потом переводы для параграфов, где использован paragraphs_asymmetric_translation_widgets, перестают работать.
Sia не публиковал свой патч, т.к. пока это прототип, и весь код написан в tmgmt модуле. Но со мной он поделился своим решением.
Публикую в виде патча.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
diff --git docroot/modules/contrib/tmgmt/sources/content/src/Plugin/tmgmt/Source/ContentEntitySource.php docroot/modules/contrib/tmgmt/sources/content/src/Plugin/tmgmt/Source/ContentEntitySource.php index 45d8763612..0c528bbfbb 100644 --- docroot/modules/contrib/tmgmt/sources/content/src/Plugin/tmgmt/Source/ContentEntitySource.php +++ docroot/modules/contrib/tmgmt/sources/content/src/Plugin/tmgmt/Source/ContentEntitySource.php @@ -486,14 +486,19 @@ public function getExistingLangCodes(JobItemInterface $job_item) { * @param bool $save * (optional) Whether to save the translation or not. * + * @throws \Exception + * Thrown when a field or field offset is missing. */ protected function doSaveTranslations(ContentEntityInterface $entity, array $data, $target_langcode, JobItemInterface $item, $save = TRUE) { - // If the translation for this language does not exist yet, initialize it. - if (!$entity->hasTranslation($target_langcode)) { - $entity->addTranslation($target_langcode, $entity->toArray()); + // If the translation for this language already exists, we need to update it + // rather than create a new one. + if ($entity->hasTranslation($target_langcode)) { + $translation = $entity->getTranslation($target_langcode); + } + else { + $translation = $entity->addTranslation($target_langcode, $entity->toArray()); } - $translation = $entity->getTranslation($target_langcode); $manager = \Drupal::service('content_translation.manager'); if ($manager->isEnabled($translation->getEntityTypeId(), $translation->bundle())) { $manager->getTranslationMetadata($translation)->setSource($entity->language()->getId()); @@ -503,17 +508,7 @@ protected function doSaveTranslations(ContentEntityInterface $entity, array $dat $field_data = $data[$field_name]; if (!$translation->hasField($field_name)) { - $message = 'Skipping field %field for job item <a href="@link">@item</a> because it does not exist on entity <em>@type/@id</em>.'; - $args = [ - '%field' => $field_name, - '@link' => $item->toUrl()->toString(), - '@item' => $item->id(), - '@type' => $translation->getEntityTypeId(), - '@id' => $translation->id(), - ]; - $item->addMessage($message, $args, 'warning'); - - continue; + throw new \Exception("Field '$field_name' does not exist on entity " . $translation->getEntityTypeId() . '/' . $translation->id()); } $field = $translation->get($field_name); @@ -523,7 +518,6 @@ protected function doSaveTranslations(ContentEntityInterface $entity, array $dat $embeddable_fields = static::getEmbeddableFields($entity); foreach ($embeddable_fields as $field_name => $field_definition) { - if (!isset($data[$field_name])) { continue; } @@ -531,50 +525,31 @@ protected function doSaveTranslations(ContentEntityInterface $entity, array $dat $field = $translation->get($field_name); $target_type = $field->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type'); $is_target_type_translatable = $manager->isEnabled($target_type); - // Special handling for asymmetric paragraphs translations. - // @see https://www.drupal.org/project/paragraphs_asymmetric_translation_widgets - // @see https://www.drupal.org/project/tmgmt/issues/3134922 $is_asymmetric_translation_mode = $target_type == 'paragraph' && $field->getFieldDefinition()->getFieldStorageDefinition()->isTranslatable(); - // In case the target type is not translatable, the referenced entity will - // be duplicated. As a consequence, remove all the field items from the - // translation, update the field value to use the field object from the - // source language. - if (!$is_target_type_translatable) { - $field = clone $entity->get($field_name); - - if (!$translation->get($field_name)->isEmpty()) { - $translation->set($field_name, NULL); - } - } - foreach (Element::children($data[$field_name]) as $delta) { - $field_item = $data[$field_name][$delta]; - foreach (Element::children($field_item) as $property) { - // Find the referenced entity. In case we are dealing with - // untranslatable target types, the source entity will be returned. - if ($target_entity = $this->findReferencedEntity($field, $field_item, $delta, $property, $is_target_type_translatable, $is_asymmetric_translation_mode)) { - if ($is_target_type_translatable) { - // If the field is an embeddable reference and the property is a - // content entity, process it recursively. - - // If the field is ERR and the target entity supports - // the needs saving interface, do not save it immediately to avoid - // creating two versions when content moderation is used but just - // ensure it will be saved. - $target_save = TRUE; - if ($field->getFieldDefinition()->getType() == 'entity_reference_revisions' && $target_entity instanceof EntityNeedsSaveInterface) { - $target_save = FALSE; - $target_entity->needsSave(); + // Handle asymmetric paragraphs translations + if ($is_asymmetric_translation_mode) { + $this->handleAsymmetricParagraphsTranslation($translation, $field_name, $data[$field_name], $target_langcode, $item); + } + else { + // Original logic for non-asymmetric fields + foreach (Element::children($data[$field_name]) as $delta) { + $field_item = $data[$field_name][$delta]; + foreach (Element::children($field_item) as $property) { + if ($target_entity = $this->findReferencedEntity($field, $field_item, $delta, $property, $is_target_type_translatable, $is_asymmetric_translation_mode)) { + if ($is_target_type_translatable) { + $target_save = TRUE; + if ($field->getFieldDefinition()->getType() == 'entity_reference_revisions' && $target_entity instanceof EntityNeedsSaveInterface) { + $target_save = FALSE; + $target_entity->needsSave(); + } + $this->doSaveTranslations($target_entity, $field_item[$property], $target_langcode, $item, $target_save); + } + else { + $duplicate = $this->createTranslationDuplicate($target_entity, $target_langcode); + $this->doSaveTranslations($duplicate, $field_item[$property], $target_langcode, $item, FALSE); + $translation->get($field_name)->set($delta, $duplicate); } - - $this->doSaveTranslations($target_entity, $field_item[$property], $target_langcode, $item, $target_save); - } - else { - $duplicate = $this->createTranslationDuplicate($target_entity, $target_langcode); - // Do not save the duplicate as it's going to be saved with the - // main entity. - $this->doSaveTranslations($duplicate, $field_item[$property], $target_langcode, $item, FALSE); - $translation->get($field_name)->set($delta, $duplicate); } } } @@ -627,7 +602,96 @@ protected function doSaveTranslations(ContentEntityInterface $entity, array $dat $translation->save(); } } +/** + * Handles asymmetric paragraphs translation. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $translation + * The entity translation. + * @param string $field_name + * The field name. + * @param array $field_data + * The field data. + * @param string $target_langcode + * The target language code. + * @param \Drupal\tmgmt\JobItemInterface $item + * The job item. + */ + protected function handleAsymmetricParagraphsTranslation(ContentEntityInterface $translation, $field_name, array $field_data, $target_langcode, JobItemInterface $item) { + $field = $translation->get($field_name); + $new_field_value = []; + $paragraph_storage = \Drupal::entityTypeManager()->getStorage('paragraph'); + + foreach (Element::children($field_data) as $delta) { + $field_item = $field_data[$delta]; + foreach (Element::children($field_item) as $property) { + $target_entity = $this->findReferencedEntity($field, $field_item, $delta, $property, TRUE, TRUE); + if ($target_entity) { + // Check if a translation already exists + if ($target_entity->hasTranslation($target_langcode)) { + $duplicate = $target_entity->getTranslation($target_langcode); + } else { + $duplicate = $this->createAsymmetricParagraphDuplicate($target_entity, $target_langcode); + } + + $this->doSaveTranslations($duplicate, $field_item[$property], $target_langcode, $item, FALSE); + + // Ensure the duplicate is saved before adding it to the field + if ($duplicate->isNew()) { + $duplicate->save(); + } + + $new_field_value[] = [ + 'target_id' => $duplicate->id(), + 'target_revision_id' => $duplicate->getRevisionId(), + ]; + } + } + } + + // Set the new field value + $translation->set($field_name, $new_field_value); + + // Log the number of paragraphs processed + \Drupal::logger('tmgmt_content')->info('Processed @count paragraphs for field @field in @entity_type @entity_id', [ + '@count' => count($new_field_value), + '@field' => $field_name, + '@entity_type' => $translation->getEntityTypeId(), + '@entity_id' => $translation->id(), + ]); + } + /** + * Creates an asymmetric paragraph duplicate. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $paragraph + * The original paragraph entity. + * @param string $target_langcode + * The target language code. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * The duplicated paragraph entity. + */ + protected function createAsymmetricParagraphDuplicate(ContentEntityInterface $paragraph, $target_langcode) { + $duplicate = $paragraph->createDuplicate(); + $duplicate->set($duplicate->getEntityType()->getKey('langcode'), $target_langcode); + + // Recursively duplicate nested paragraphs + foreach ($duplicate->getFields() as $field_name => $field) { + if ($field->getFieldDefinition()->getType() == 'entity_reference_revisions' && + $field->getFieldDefinition()->getSetting('target_type') == 'paragraph') { + $new_field_value = []; + foreach ($field as $delta => $item) { + if ($item->entity) { + $nested_duplicate = $this->createAsymmetricParagraphDuplicate($item->entity, $target_langcode); + $new_field_value[] = ['entity' => $nested_duplicate]; + } + } + $duplicate->set($field_name, $new_field_value); + } + } + + return $duplicate; + } /** * Creates a translation duplicate of the given entity. * |
Я тестировал данный патч, и он решает проблему с переводами для меня. Асимметричный параграфы снова начинают переводиться, а данные корректно сохраняются в базу. Буду ждать окончательную версию, если Sia когда-нибудь выложит её.