diff --git a/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
index bc4fae3..270a750 100644
--- a/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
+++ b/src/Plugin/Field/FieldWidget/InlineParagraphsWidget.php
@@ -4,7 +4,7 @@ namespace Drupal\paragraphs\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Html;
-use Drupal\Core\Entity\Entity;
+use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
@@ -14,7 +14,9 @@ use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Render\Element;
+use Drupal\field\Entity\FieldConfig;
use Drupal\paragraphs;
+use Drupal\paragraphs\Entity\Paragraph;
use Symfony\Component\Validator\ConstraintViolationInterface;
@@ -166,7 +168,6 @@ class InlineParagraphsWidget extends WidgetBase {
$parents = $element['#field_parents'];
$paragraphs_entity = NULL;
- $host = $items->getEntity();
$widget_state = static::getWidgetState($parents, $field_name, $form_state);
$entity_manager = \Drupal::entityTypeManager();
@@ -215,48 +216,7 @@ class InlineParagraphsWidget extends WidgetBase {
}
if ($paragraphs_entity) {
- // Detect if we are translating.
- $this->initIsTranslating($form_state, $host);
- $langcode = $form_state->get('langcode');
-
- if (!$this->isTranslating) {
- // Set the langcode if we are not translating.
- $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
- if ($paragraphs_entity->get($langcode_key)->value != $langcode) {
- // If a translation in the given language already exists, switch to
- // that. If there is none yet, update the language.
- if ($paragraphs_entity->hasTranslation($langcode)) {
- $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
- }
- else {
- $paragraphs_entity->set($langcode_key, $langcode);
- }
- }
- }
- else {
- // Add translation if missing for the target language.
- if (!$paragraphs_entity->hasTranslation($langcode)) {
- // Get the selected translation of the paragraph entity.
- $entity_langcode = $paragraphs_entity->language()->getId();
- $source = $form_state->get(['content_translation', 'source']);
- $source_langcode = $source ? $source->getId() : $entity_langcode;
- $paragraphs_entity = $paragraphs_entity->getTranslation($source_langcode);
- // The paragraphs entity has no content translation source field if
- // no paragraph entity field is translatable, even if the host is.
- if ($paragraphs_entity->hasField('content_translation_source')) {
- // Initialise the translation with source language values.
- $paragraphs_entity->addTranslation($langcode, $paragraphs_entity->toArray());
- $translation = $paragraphs_entity->getTranslation($langcode);
- $manager = \Drupal::service('content_translation.manager');
- $manager->getTranslationMetadata($translation)->setSource($paragraphs_entity->language()->getId());
- }
- }
- // If any paragraphs type is translatable do not switch.
- if ($paragraphs_entity->hasField('content_translation_source')) {
- // Switch the paragraph to the translation.
- $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
- }
- }
+ $paragraphs_entity = $this->prepareEntity($paragraphs_entity, $items, $form_state);
$element_parents = $parents;
$element_parents[] = $field_name;
@@ -343,7 +303,7 @@ class InlineParagraphsWidget extends WidgetBase {
}
// Hide the button when translating.
- $button_access = $paragraphs_entity->access('delete') && !$this->isTranslating;
+ $button_access = $paragraphs_entity->access('delete') && (!$this->isTranslating || $items->getFieldDefinition()->isTranslatable());
$element['top']['paragraphs_remove_button_container']['paragraphs_remove_button'] = [
'#type' => 'submit',
'#value' => $this->t('Remove'),
@@ -773,7 +733,7 @@ class InlineParagraphsWidget extends WidgetBase {
foreach ($bundles as $machine_name => $bundle) {
if ($dragdrop_settings || (!count($this->getSelectionHandlerSetting('target_bundles'))
- || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
+ || in_array($machine_name, $this->getSelectionHandlerSetting('target_bundles')))) {
$options[$machine_name] = $bundle['label'];
if ($access_control_handler->createAccess($machine_name)) {
@@ -834,7 +794,7 @@ class InlineParagraphsWidget extends WidgetBase {
$elements['add_more'] = array(
'#type' => 'container',
'#theme_wrappers' => array('paragraphs_dropbutton_wrapper'),
- '#access' => !$this->isTranslating,
+ '#access' => (!$this->isTranslating || $items->getFieldDefinition()->isTranslatable()),
);
if (count($access_options)) {
@@ -870,7 +830,7 @@ class InlineParagraphsWidget extends WidgetBase {
);
if ($drop_button) {
$elements['add_more']['add_more_button_' . $machine_name]['#prefix'] = '
';
- $elements['add_more']['add_more_button_' . $machine_name]['#suffix'] = '';
+ $elements['add_more']['add_more_button_' . $machine_name]['#suffix'] = '';
}
}
}
@@ -1164,6 +1124,83 @@ class InlineParagraphsWidget extends WidgetBase {
}
/**
+ * Prepares the paragraph entity for translation.
+ *
+ * @param \Drupal\paragraphs\Entity\Paragraph $entity
+ * The paragraph entity.
+ * @param \Drupal\Core\Field\FieldItemListInterface $items
+ * The field items list that hosts this paragraph.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return \Drupal\paragraphs\Entity\Paragraph
+ * The prepared paragraph.
+ *
+ * @see \Drupal\Core\Entity\ContentEntityForm::initFormLangcodes().
+ */
+ protected function prepareEntity(Paragraph $entity, FieldItemListInterface $items, FormStateInterface $form_state) {
+ // Detect if we are translating.
+ $this->initIsTranslating($form_state, $items->getEntity());
+ $langcode = $form_state->get('langcode');
+
+ if (!$this->isTranslating) {
+ // Set the langcode if we are not translating.
+ $langcode_key = $entity->getEntityType()->getKey('langcode');
+ if ($entity->get($langcode_key)->value != $langcode) {
+ // If a translation in the given language already exists, switch to
+ // that. If there is none yet, update the language.
+ if ($entity->hasTranslation($langcode)) {
+ $entity = $entity->getTranslation($langcode);
+ }
+ else {
+ $entity->set($langcode_key, $langcode);
+ }
+ }
+ }
+
+ // Localised Paragraphs.
+ // If the parent field is marked as translatable, assume paragraphs
+ // to be localized (host entity expects different paragraphs for
+ // different languages)
+ elseif ($items->getFieldDefinition()->isTranslatable()) {
+ $entity = $this->cloneReferencedEntity($entity, $langcode);
+ }
+
+ // Translated Paragraphs
+ // If the parent field is not translatable, assume the paragraph
+ // entity itself (rather the fields within it) are marked as
+ // translatable. (host entity expects same paragraphs in different
+ // languages).
+ else {
+ // Add translation if missing for the target language.
+ if (!$entity->hasTranslation($langcode)) {
+ // Get the selected translation of the paragraph entity.
+ $entity_langcode = $entity->language()->getId();
+ $source = $form_state->get(['content_translation', 'source']);
+ $source_langcode = $source ? $source->getId() : $entity_langcode;
+ $entity = $entity->getTranslation($source_langcode);
+ // The paragraphs entity has no content translation source field if
+ // no paragraph entity field is translatable, even if the host is.
+ if ($entity->hasField('content_translation_source')) {
+ // Initialise the translation with source language values.
+ $entity->addTranslation($langcode, $entity->toArray());
+ $translation = $entity->getTranslation($langcode);
+ $manager = \Drupal::service('content_translation.manager');
+ $manager->getTranslationMetadata($translation)
+ ->setSource($entity->language()->getId());
+ }
+ }
+ // If any paragraphs type is translatable do not switch.
+ if ($entity->hasField('content_translation_source')) {
+ // Switch the paragraph to the translation.
+ $entity = $entity->getTranslation($langcode);
+ }
+ }
+
+ return $entity;
+ }
+
+/**
* Initializes the translation form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
@@ -1278,4 +1315,62 @@ class InlineParagraphsWidget extends WidgetBase {
return strip_tags($collapsed_summary_text);
}
+ /**
+ * Clones Paragraphs (and field_collections) recursively, preparing them to be
+ * passed to the translated paragraph widget.
+ *
+ * @param \Drupal\Core\Entity\ContentEntityInterface $entity_to_clone The Entity
+ * to clone
+ * @param string $langcode language code for all the clone entities created.
+ * @return \Drupal\Core\Entity\ContentEntityInterface New entity object which
+ * has the same data as the original $entity_to_clone, Note this entity is not
+ * saved.
+ */
+ protected function cloneReferencedEntity(ContentEntityInterface $entity_to_clone, $langcode) {
+ $entity_manager = \Drupal::entityTypeManager();
+ // Get the paragraph item as an array of values.
+ $paragraph_array = $entity_to_clone->toArray();
+ $target_type = $entity_to_clone->getEntityTypeId();
+ $entity_type = $entity_manager->getDefinition($target_type);
+ $bundle_key = $entity_type->getKey('bundle');
+
+ // Create a new entity for this language.
+ $new_entity = array(
+ $bundle_key => $entity_to_clone->bundle(),
+ 'langcode' => $langcode
+ );
+
+ // Loop through all fields in the paragraph and add to new entity.
+ foreach ($entity_to_clone->getFieldDefinitions() as $field_name => $field_definition) {
+ // Check that the value is a field config and not empty.
+ if ($field_definition instanceof FieldConfig && !empty($paragraph_array[$field_name])) {
+ if ($this->checkEntityTypeCloneable($field_definition->getSetting('target_type'))) {
+ /** @var [EntityInterface] $entities */
+ $entities = $entity_to_clone->get($field_name)->referencedEntities();
+ $cloned_entites = [];
+ foreach ($entities as $entity){
+ $cloned_entites[] = $this->cloneReferencedEntity($entity, $langcode);
+ }
+ $new_entity[$field_name] = $cloned_entites;
+ }
+ else {
+ $new_entity[$field_name] = $paragraph_array[$field_name];
+ }
+ }
+ }
+ return $entity_manager->getStorage($target_type)->create($new_entity);
+ }
+
+ /**
+ * Checks whether we support cloning a certain entity type or not.
+ *
+ * @param string $entity_type_id the entity type ID to check whether it's cloneable
+ * @return bool
+ */
+ protected function checkEntityTypeCloneable($entity_type_id){
+ // @todo: maybe this list should be moved to widget configs with some sensible
+ // default?
+ return in_array($entity_type_id, ['field_collection_item', 'paragraph']);
+ }
+
}
diff --git a/src/Tests/ParagraphsAssymetricTranslationTest.php b/src/Tests/ParagraphsAssymetricTranslationTest.php
new file mode 100644
index 0000000..f3a071d
--- /dev/null
+++ b/src/Tests/ParagraphsAssymetricTranslationTest.php
@@ -0,0 +1,135 @@
+drupalPlaceBlock('local_tasks_block');
+ $this->drupalPlaceBlock('page_title_block');
+
+ $this->admin_user = $this->drupalCreateUser(
+ [
+ 'administer site configuration',
+ 'administer nodes',
+ 'create paragraphed_content_demo content',
+ 'edit any paragraphed_content_demo content',
+ 'delete any paragraphed_content_demo content',
+ 'administer paragraph form display',
+ 'administer paragraph fields',
+ 'administer content translation',
+ 'translate any entity',
+ 'create content translations',
+ 'administer languages',
+ 'administer content types',
+ ]
+ );
+
+ $this->drupalLogin($this->admin_user);
+
+ // Mark the paragraph entities as untranslatable and the paragraph field
+ // as translatable.
+ $edit = [
+ 'entity_types[paragraph]' => FALSE,
+ 'settings[node][paragraphed_content_demo][fields][field_paragraphs_demo]' => TRUE,
+ 'settings[paragraph][images][translatable]' => FALSE,
+ 'settings[paragraph][images][settings][language][language_alterable]' => FALSE,
+ 'settings[paragraph][image_text][translatable]' => FALSE,
+ 'settings[paragraph][image_text][settings][language][language_alterable]' => FALSE,
+ 'settings[paragraph][nested_paragraph][translatable]' => FALSE,
+ 'settings[paragraph][nested_paragraph][settings][language][language_alterable]' => FALSE,
+ 'settings[paragraph][text][translatable]' => FALSE,
+ 'settings[paragraph][text][settings][language][language_alterable]' => FALSE,
+ 'settings[paragraph][text_image][translatable]' => FALSE,
+ 'settings[paragraph][text_image][settings][language][language_alterable]' => FALSE,
+ 'settings[paragraph][user][translatable]' => FALSE,
+ 'settings[paragraph][user][settings][language][language_alterable]' => FALSE,
+ ];
+ $this->drupalPostForm(
+ 'admin/config/regional/content-language',
+ $edit,
+ t('Save configuration')
+ );
+ }
+
+ /**
+ * Test asymmetric translation.
+ */
+ public function testParagraphsMultilingualFieldTranslation() {
+ // Add an English node.
+ $this->drupalGet('node/add/paragraphed_content_demo');
+ $this->drupalPostForm(NULL, NULL, t('Add Text'));
+
+ $edit = [
+ 'title[0][value]' => 'Title in english',
+ 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Text in english',
+ ];
+ $this->drupalPostForm(NULL, $edit, t('Save and publish'));
+
+ // Translate the node to French.
+ $this->clickLink(t('Translate'));
+ $this->clickLink(t('Add'), 1);
+
+ $edit = [
+ 'title[0][value]' => 'Title in french',
+ 'field_paragraphs_demo[0][subform][field_text_demo][0][value]' => 'Text in french',
+ 'revision' => TRUE,
+ 'revision_log[0][value]' => 'french 1',
+ ];
+ $this->drupalPostForm(
+ NULL,
+ $edit,
+ t('Save and keep published (this translation)')
+ );
+
+ $node = $this->drupalGetNodeByTitle('Title in english');
+
+ // Check the english translation.
+ $this->drupalGet('node/' . $node->id());
+ $this->assertText('Title in english');
+ $this->assertText('Text in english');
+ $this->assertNoText('Title in french');
+ $this->assertNoText('Text in french');
+
+ // Check the french translation.
+ $this->drupalGet('fr/node/' . $node->id());
+ $this->assertText('Title in french');
+ $this->assertText('Text in french');
+ $this->assertNoText('Title in english');
+ $this->assertNoText('Text in english');
+
+ $select = \Drupal::database()->select('node__field_paragraphs_demo', 'p');
+ $select->addField('p', 'langcode');
+ $select->condition('p.entity_id', $node->id());
+ $paragraph_langcodes = $select->execute()->fetchCol();
+
+ $this->assertEqual(
+ $paragraph_langcodes,
+ ['en', 'fr'],
+ 'Translated paragraphs are separate entities'
+ );
+ }
+
+}