diff --git a/composer.json b/composer.json index 382fff7a..bf12ffa2 100644 --- a/composer.json +++ b/composer.json @@ -20,14 +20,18 @@ "drupal/config_devel": "^1.2", "drupal/core-composer-scaffold": "^9.4 || ^10", "drupal/core-dev": "^9.4 || ^10", + "drupal/description_list_field": "^1.0@alpha", "drupal/entity_version": "^1.0-beta8", + "drupal/link_description": "^1.0", "drupal/metatag": "^1.16", "drupal/paragraphs": "^1.13", + "drupal/typed_link": "^2.0", "drush/drush": "^11.1", "openeuropa/code-review": "^2.0", "openeuropa/epoetry-client": "1.x-dev || 2.x-dev", - "openeuropa/oe_multilingual": "^1.13", + "openeuropa/oe_content": "^3.0.0-beta2", "openeuropa/oe_editorial": "^2.0", + "openeuropa/oe_multilingual": "dev-master", "openeuropa/task-runner-drupal-project-symlink": "^1.0-beta6", "phpspec/prophecy-phpunit": "^2", "symfony/property-access": "^4 || ^5.4 || ^6", diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/README.md b/modules/oe_translation_local/modules/oe_translation_multivalue/README.md new file mode 100644 index 00000000..00a8ef86 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/README.md @@ -0,0 +1,44 @@ +# OpenEuropa Translation Multivalue + +This submodule can be used to solve a very particular, but annoying, issue with the way the local translation system works. + +## The problem (scenario) + +* You have a node with a translatable multivalue field, with 2 values. The key here is that it needs to be multivalue, but not an entity reference. A regular one like Textfield or Link. +* You translate the node and its multivalue field values. +* You edit the node and you reorder the values in the multivalue field. +* You translate again using local translation and the pre-filled translation values no longer matches the delta for the multivalue field. Because in the previous version the deltas were reversed. So if you save the translation without paying +attention, you'll end up with mixed up translation values. + +This problem is caused by the fact on the local translation form, the system does a best effort to pre-fill with translation values from the previous content version. Most of the time, it manages. Even with values that are within +multiple referenced entities. It cannot, however, do so on simple multivalue fields because it cannot guess that they were reversed or any added. All it can check is the delta. + +## The solution + +Installing the current module gives the possibility to add a new column to multivalue fields called `translation_id`. So whenever a value is saved, a unique ID is generated for it. And based on this ID, the +system can then track which value is at which delta to prefill. + +## How it works + +It works by overriding the field item class for a given field and adding a new table column and property to track this translation ID. Moreover, it does the handling for saving this ID when synchronising the translations +as well. + +## How to use + +Go to the storage settings of a multivalue translatable field and check the box `Translation multivalue`. This will create the column and add the property. + +Note that this only works for fields which don't have any data inside. + +If you already have data in the field, or you have an existing site where you need to turn this feature one, you need to do an update path. For this, there is a helper method: `TranslationMultivalueColumnInstaller::installColumn`. + +To this helper you need to pass the field name in the format `entity_type.field_name`. If you do this on multiple fields with lots of data, make sure you batch this per field to avoid problems. + +**Note that for the update, the process creates a backup table, truncates the field tables, adds the column and then sets +back the values. So make sure you thoroughly test your process before deploying to production to avoid any issues**. + +## Support + +Currently, only certain field types are supported. You can check `oe_translation_multivalue_field_types()` for which field types are altered for this. + +If you need support for other field types, open an issue or a ticket on the EWPP board. + diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/config/schema/oe_translation_multivalue.schema.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/config/schema/oe_translation_multivalue.schema.yml new file mode 100644 index 00000000..02c5d0f4 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/config/schema/oe_translation_multivalue.schema.yml @@ -0,0 +1,23 @@ +field.storage_settings.description_list_field: + type: mapping + label: 'Description list storage settings' + mapping: + translation_multivalue: + type: boolean + label: Translation multivalue + +field.storage_settings.address: + type: mapping + label: 'Address storage settings' + mapping: + translation_multivalue: + type: boolean + label: Translation multivalue + +field.storage_settings.timeline_field: + type: mapping + label: 'Timeline storage settings' + mapping: + translation_multivalue: + type: boolean + label: Translation multivalue diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.info.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.info.yml new file mode 100644 index 00000000..0d769d78 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.info.yml @@ -0,0 +1,8 @@ +name: OpenEuropa Translation Local Multivalue +description: Provides IDs for the values of multivalue fields to aid in the pre-filling of translation values in the local translation form +package: OpenEuropa + +type: module +core_version_requirement: ^9.4 || ^10 +dependencies: + - oe_translation:oe_translation_local diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.module b/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.module new file mode 100644 index 00000000..f47b4674 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.module @@ -0,0 +1,260 @@ +setFormClass('local_translation', LocalTranslationRequestForm::class); +} + +/** + * Implements hook_element_info_alter(). + */ +function oe_translation_multivalue_element_info_alter(array &$info) { + if (isset($info['address'])) { + $info['address']['#process'][] = 'oe_translation_multivalue_address_process'; + } +} + +/** + * Processor for the Address element. + * + * We need to add a hidden form value on the element with a default value + * we set in the widget alter. + * + * @param array $element + * The element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param array $complete_form + * The form. + * + * @return array + * The element. + */ +function oe_translation_multivalue_address_process(array &$element, FormStateInterface $form_state, array &$complete_form) { + if (!isset($element['#default_value']['translation_id'])) { + return $element; + } + $element['translation_id'] = [ + '#type' => 'hidden', + '#value' => $element['#default_value']['translation_id'], + ]; + return $element; +} + +/** + * Get a list of the form widgets that should be altered. + * + * These are the widgets we alter the form of in order to put a hidden + * translation_id form element with a random ID. + * + * @return string[] + * The widget IDs. + */ +function oe_translation_multivalue_widgets() { + $field_types = oe_translation_multivalue_field_types(); + $options = []; + $plugin_manager = \Drupal::service('plugin.manager.field.widget'); + foreach ($field_types as $field_type) { + $options += $plugin_manager->getOptions($field_type); + } + + return array_keys($options); +} + +/** + * Get a list of the field types we are altering. + * + * @todo make this extendable. + * + * @return string[] + * The field IDs. + */ +function oe_translation_multivalue_field_types() { + return [ + 'link', + 'address', + 'description_list_field', + 'link_description', + 'string', + 'timeline_field', + 'typed_link', + ]; +} + +/** + * Implements hook_field_widget_complete_form_alter(). + */ +function oe_translation_multivalue_field_widget_complete_form_alter(&$field_widget_complete_form, FormStateInterface $form_state, $context) { + $widgets = oe_translation_multivalue_widgets(); + $widget = $context['widget']; + if (!in_array($widget->getPluginId(), $widgets)) { + return; + } + $items = $context['items']; + $property_definitions = $items->getFieldDefinition()->getItemDefinition()->getPropertyDefinitions(); + if (!isset($property_definitions['translation_id'])) { + return; + } + + $id_generator = \Drupal::service('oe_translation_multivalue.translation_id_generator'); + $field_id = $items->getFieldDefinition()->getFieldStorageDefinition()->id(); + foreach ($items as $delta => $item) { + // If the field delta doesn't yet have a translation ID, generate the next + // one and set that. However, if we do generate a new one, we need to also + // increment it here with each iteration because each delta needs to + // receive a new one. + $translation_id = $item->translation_id && Uuid::isValid($item->translation_id) ? $item->translation_id : NULL; + if (!$translation_id) { + $translation_id = $id_generator->generateTranslationUuid($field_id); + } + + // For address, we have an exception because it uses a form element. + if ($widget->getPluginId() === 'address_default') { + $field_widget_complete_form['widget'][$delta]['address']['#default_value']['translation_id'] = $translation_id; + continue; + } + $field_widget_complete_form['widget'][$delta]['translation_id'] = [ + '#type' => 'hidden', + '#value' => $translation_id, + ]; + } +} + +/** + * Implements hook_entity_presave(). + * + * When we save a content entity that may have fields that contain the + * translation_id column, check if by any chance an attempt is being made to + * save them without one. If so, set a value. This can happen when entities + * are created programmatically. + */ +function oe_translation_multivalue_entity_presave(EntityInterface $entity) { + if (!$entity instanceof ContentEntityInterface) { + return; + } + + $field_types = oe_translation_multivalue_field_types(); + + $field_definitions = $entity->getFieldDefinitions(); + $fields = []; + foreach ($field_definitions as $field_name => $field_definition) { + if (in_array($field_definition->getType(), $field_types)) { + $fields[] = $field_name; + } + } + + $id_generator = \Drupal::service('oe_translation_multivalue.translation_id_generator'); + + foreach ($fields as $field) { + if ($entity->get($field)->isEmpty()) { + continue; + } + + if (!in_array('translation_id', $entity->get($field)->getFieldDefinition()->getFieldStorageDefinition()->getPropertyNames())) { + continue; + } + + $field_id = $entity->getEntityTypeId() . '.' . $field; + foreach ($entity->get($field) as $item) { + if (!isset($item->getProperties()['translation_id'])) { + continue; + } + + if ($item->translation_id) { + continue; + } + + $item->translation_id = $id_generator->generateTranslationUuid($field_id); + } + } +} + +/** + * Implements hook_field_info_alter(). + */ +function oe_translation_multivalue_field_info_alter(&$info) { + if (isset($info['link'])) { + $info['link']['class'] = LinkItemMultiple::class; + $info['link']['oe_translation_source_field_processor'] = MultivalueTranslationSourceFieldProcessor::class; + } + + if (isset($info['description_list_field'])) { + $info['description_list_field']['class'] = DescriptionListItemMultiple::class; + $info['description_list_field']['oe_translation_source_field_processor'] = DescriptionListTranslationMultivalueSourceFieldProcessor::class; + } + + if (isset($info['timeline_field'])) { + $info['timeline_field']['class'] = TimelineItemMultiple::class; + $info['timeline_field']['oe_translation_source_field_processor'] = TimelineTranslationMultivalueSourceFieldProcessor::class; + } + + if (isset($info['address'])) { + $info['address']['class'] = AddressItemMultiple::class; + $info['address']['oe_translation_source_field_processor'] = AddressTranslationMultivalueSourceFieldProcessor::class; + } + + if (isset($info['typed_link'])) { + $info['typed_link']['class'] = TypedLinkItemMultiple::class; + $info['typed_link']['oe_translation_source_field_processor'] = MultivalueTranslationSourceFieldProcessor::class; + } + + if (isset($info['string'])) { + $info['string']['class'] = StringItemMultiple::class; + $info['string']['oe_translation_source_field_processor'] = MultivalueTranslationSourceFieldProcessor::class; + } + + if (isset($info['link_description'])) { + $info['link_description']['class'] = LinkDescriptionItemMultiple::class; + $info['link_description']['oe_translation_source_field_processor'] = MultivalueTranslationSourceFieldProcessor::class; + } +} + +/** + * Implements hook_config_schema_info_alter(). + */ +function oe_translation_multivalue_config_schema_info_alter(&$definitions) { + $field_types = oe_translation_multivalue_field_types(); + foreach ($field_types as $plugin_id) { + $schema_id = 'field.storage_settings.' . $plugin_id; + if (!isset($definitions[$schema_id])) { + // If the schema doesn't exist already, we cannot add it as we get an + // exception. For those, we need to define them in the schema yml. + continue; + } + $definitions[$schema_id]['mapping']['translation_multivalue'] = [ + 'type' => 'boolean', + 'label' => 'Translation multivalue', + ]; + } +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.services.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.services.yml new file mode 100644 index 00000000..8525ce72 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/oe_translation_multivalue.services.yml @@ -0,0 +1,4 @@ +services: + oe_translation_multivalue.translation_id_generator: + class: Drupal\oe_translation_multivalue\MultivalueTranslationIdGenerator + arguments: ['@entity_type.manager', '@uuid', '@database'] diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/AddressTranslationMultivalueSourceFieldProcessor.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/AddressTranslationMultivalueSourceFieldProcessor.php new file mode 100644 index 00000000..b822f942 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/AddressTranslationMultivalueSourceFieldProcessor.php @@ -0,0 +1,24 @@ +setMultivalueTranslations($field_data, $field); + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/DescriptionListTranslationMultivalueSourceFieldProcessor.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/DescriptionListTranslationMultivalueSourceFieldProcessor.php new file mode 100644 index 00000000..066972b0 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/DescriptionListTranslationMultivalueSourceFieldProcessor.php @@ -0,0 +1,24 @@ +setMultivalueTranslations($field_data, $field); + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/FieldItemOverrides/AddressItemMultiple.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/FieldItemOverrides/AddressItemMultiple.php new file mode 100644 index 00000000..d41bf7dc --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/FieldItemOverrides/AddressItemMultiple.php @@ -0,0 +1,31 @@ + FALSE, + ] + parent::defaultStorageSettings(); + } + + /** + * {@inheritdoc} + */ + public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { + $elements = []; + $elements['translation_multivalue'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Translation multivalue'), + '#default_value' => $this->getSetting('translation_multivalue'), + '#description' => $this->t('Whether to keep a translation ID of each value in the field to aid in local translations.'), + '#disabled' => $has_data, + ]; + + return $elements + parent::storageSettingsForm($form, $form_state, $has_data); + } + + /** + * Overrides the property definitions. + */ + protected static function overridePropertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties = parent::propertyDefinitions($field_definition); + + $enabled = (bool) $field_definition->getSetting('translation_multivalue'); + + if ((int) $field_definition->getCardinality() === 1 || !$enabled) { + return $properties; + } + + $properties['translation_id'] = DataDefinition::create('string') + ->setLabel(new TranslatableMarkup('Translation ID')); + + return $properties; + } + + /** + * Overrides the schema. + */ + protected static function overrideSchema(FieldStorageDefinitionInterface $field_definition) { + $schema = parent::schema($field_definition); + + $enabled = (bool) $field_definition->getSetting('translation_multivalue'); + + if ((int) $field_definition->getCardinality() === 1 || !$enabled) { + return $schema; + } + + $schema['columns']['translation_id'] = [ + 'description' => 'The translation ID.', + 'type' => 'varchar', + 'length' => 128, + 'unique keys' => [ + 'translation_id' => 'translation_id', + ], + 'not null' => FALSE, + ]; + + return $schema; + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/FieldItemOverrides/TypedLinkItemMultiple.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/FieldItemOverrides/TypedLinkItemMultiple.php new file mode 100644 index 00000000..18769ac7 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/FieldItemOverrides/TypedLinkItemMultiple.php @@ -0,0 +1,31 @@ +entityFieldManager = $entityFieldManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.repository'), + $container->get('entity_type.bundle.info'), + $container->get('datetime.time'), + $container->get('entity_type.manager'), + $container->get('oe_translation.translation_source_manager'), + $container->get('current_user'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + protected function getExistingTranslationData(TranslationRequestLocal $translation_request, string $langcode): array { + $existing_translation_data = parent::getExistingTranslationData($translation_request, $langcode); + if (!$existing_translation_data) { + return $existing_translation_data; + } + + // Reorder the existing translation data deltas. + $data = $translation_request->getData(); + $this->processTranslationData($data, $existing_translation_data, $translation_request->getContentEntity()->getEntityTypeId()); + + return $existing_translation_data; + + } + + /** + * Processes the translation data recursively to use the correct delta. + * + * @param array $data + * The translation data. + * @param array $existing_translation_data + * The existing translation data. + * @param string $entity_type_id + * The entity type ID. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function processTranslationData(array $data, array &$existing_translation_data, string $entity_type_id): void { + foreach (Element::children($data) as $key) { + if (!isset($existing_translation_data[$key])) { + continue; + } + + $child = $data[$key]; + foreach (Element::children($child) as $child_key) { + $sub_child = $child[$child_key]; + if (isset($sub_child['entity']) && $existing_translation_data[$key]) { + $this->processTranslationData($sub_child['entity'], $existing_translation_data[$key][$child_key]['entity'], $sub_child['entity']['#entity_type']); + } + } + + if ($this->hasTranslationId($key, $entity_type_id)) { + $existing_translation_data_source = $existing_translation_data[$key]; + if (!$existing_translation_data_source) { + continue; + } + foreach (Element::children($data[$key]) as $delta) { + $translation_id = $data[$key][$delta]['translation_id']['#text']; + if (!$translation_id) { + continue; + } + $existing_translation_data[$key][$delta] = $this->getDeltaForTranslationId($translation_id, $existing_translation_data_source); + } + } + } + } + + /** + * Retrieves the delta for a given translation ID. + * + * @param string $translation_id + * The translation ID. + * @param array $existing_translation_data + * The existing translation data. + * + * @return array + * The corresponding translation data for a given ID. + */ + protected function getDeltaForTranslationId(string $translation_id, array $existing_translation_data): array { + foreach (Element::children($existing_translation_data) as $delta) { + if ($existing_translation_data[$delta]['translation_id']['#text'] === $translation_id) { + return $existing_translation_data[$delta]; + } + } + + return []; + } + + /** + * Checks if a given field name has a translation_id property. + */ + protected function hasTranslationId(string $field_name, string $entity_type_id): bool { + $field_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id); + /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ + $field_definition = $field_definitions[$field_name] ?? NULL; + if (!$field_definition) { + return FALSE; + } + + $property_names = $field_definition->getPropertyNames(); + return in_array('translation_id', $property_names); + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueFieldProcessorTrait.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueFieldProcessorTrait.php new file mode 100644 index 00000000..e4571cde --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueFieldProcessorTrait.php @@ -0,0 +1,44 @@ +offsetGet($delta)->set($property, $property_data['#text']); + } + } + } + } + + /** + * {@inheritdoc} + */ + protected function shouldTranslateProperty(TypedDataInterface $property): bool { + if ($property->getName() === 'translation_id') { + return FALSE; + } + return parent::shouldTranslateProperty($property); + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueTranslationIdGenerator.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueTranslationIdGenerator.php new file mode 100644 index 00000000..b81cfc4d --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueTranslationIdGenerator.php @@ -0,0 +1,81 @@ +entityTypeManager = $entityTypeManager; + $this->uuid = $uuid; + $this->database = $database; + } + + /** + * Generates a UUID for the translation value. + * + * We also do a check to make sure the UUID doesn't exist already in the DB. + * + * @param string $field_id + * The field ID in the format entity_type.field_name. + * + * @return string + * The UUID. + */ + public function generateTranslationUuid(string $field_id): string { + [$entity_type_id, $field_name] = explode('.', $field_id); + $table_mapping = $this->entityTypeManager->getStorage($entity_type_id)->getTableMapping(); + $table = $table_mapping->getFieldTableName($field_name); + $column = $field_name . '_translation_id'; + $exists = TRUE; + while ($exists) { + $uuid = $this->uuid->generate(); + $query = sprintf("SELECT %s FROM {%s} WHERE %s = '%s'", $column, $table, $column, $uuid); + $exists = !empty($this->database->query($query)->fetchAll()); + if (!$exists) { + // Normally, it shouldn't exist because it's a UUID, but just in case. + return $uuid; + } + } + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueTranslationSourceFieldProcessor.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueTranslationSourceFieldProcessor.php new file mode 100644 index 00000000..de69e895 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/MultivalueTranslationSourceFieldProcessor.php @@ -0,0 +1,24 @@ +setMultivalueTranslations($field_data, $field); + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/TimelineTranslationMultivalueSourceFieldProcessor.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/TimelineTranslationMultivalueSourceFieldProcessor.php new file mode 100644 index 00000000..6469c51f --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/TimelineTranslationMultivalueSourceFieldProcessor.php @@ -0,0 +1,24 @@ +setMultivalueTranslations($field_data, $field); + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/src/TranslationMultivalueColumnInstaller.php b/modules/oe_translation_local/modules/oe_translation_multivalue/src/TranslationMultivalueColumnInstaller.php new file mode 100644 index 00000000..895bb80d --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/src/TranslationMultivalueColumnInstaller.php @@ -0,0 +1,87 @@ + 'The translation ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ]; + + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping */ + $table_mapping = \Drupal::entityTypeManager()->getStorage($entity_type_id)->getTableMapping(); + $tables = $table_mapping->getAllFieldTableNames($field_name); + $column_name = $field_name . '_translation_id'; + + foreach ($tables as $table_name) { + // Backup data from original table if there is data. + $count = $database->select($table_name, 'p') + ->countQuery() + ->execute() + ->fetchField(); + + $has_data = $count > 0; + + if ($has_data) { + $original_table = '{' . $table_name . '}'; + $backup_table = "{_$table_name}"; + $query_string = 'CREATE TABLE ' . $backup_table . ' LIKE ' . $original_table; + $database->query($query_string); + $query_string = 'INSERT ' . $backup_table . ' SELECT * FROM ' . $original_table; + $database->query($query_string); + + // Wipe it. + $database->truncate($table_name)->execute(); + } + + $field_exists = $database->schema()->fieldExists($table_name, $column_name); + if (!$field_exists) { + $database->schema()->addField($table_name, $column_name, $field_schema); + } + } + + try { + // Update the field definition. + $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name); + $field_storage->setSetting('translation_multivalue', TRUE); + $field_storage->save(); + } + catch (\Exception $exception) { + // Do nothing, we want to restore the data below even if something + // crashed here. + } + + // Restore the data if we made a backup. + foreach ($tables as $table_name) { + $original_table = '{' . $table_name . '}'; + $backup_table = "{_$table_name}"; + if ($database->schema()->tableExists("_$table_name")) { + $query_string = 'INSERT ' . $original_table . ' SELECT *, NULL as ' . $column_name . ' FROM ' . $backup_table; + $database->query($query_string); + $database->query('DROP TABLE ' . $backup_table); + } + } + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_form_display.node.multivalue.default.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_form_display.node.multivalue.default.yml new file mode 100644 index 00000000..086c4e6f --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_form_display.node.multivalue.default.yml @@ -0,0 +1,160 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.multivalue.field_address + - field.field.node.multivalue.field_description_list + - field.field.node.multivalue.field_link + - field.field.node.multivalue.field_link_description + - field.field.node.multivalue.field_paragraphs + - field.field.node.multivalue.field_textfield + - field.field.node.multivalue.field_timeline + - field.field.node.multivalue.field_typed_link + - node.type.multivalue + module: + - address + - description_list_field + - link + - link_description + - oe_content_timeline_field + - paragraphs + - path + - typed_link +id: node.multivalue.default +targetEntityType: node +bundle: multivalue +mode: default +content: + created: + type: datetime_timestamp + weight: 10 + region: content + settings: { } + third_party_settings: { } + field_address: + type: address_default + weight: 122 + region: content + settings: { } + third_party_settings: { } + field_description_list: + type: description_list_widget + weight: 123 + region: content + settings: { } + third_party_settings: { } + field_link: + type: link_default + weight: 125 + region: content + settings: + placeholder_url: '' + placeholder_title: '' + third_party_settings: { } + field_link_description: + type: link_description + weight: 124 + region: content + settings: + placeholder_url: '' + placeholder_title: '' + third_party_settings: { } + field_paragraphs: + type: paragraphs + weight: 128 + region: content + settings: + title: Paragraph + title_plural: Paragraphs + edit_mode: open + closed_mode: summary + autocollapse: none + closed_mode_threshold: 0 + add_mode: dropdown + form_display_mode: default + default_paragraph_type: '' + features: + collapse_edit_all: collapse_edit_all + duplicate: duplicate + third_party_settings: { } + field_textfield: + type: string_textfield + weight: 121 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + field_timeline: + type: timeline_widget + weight: 127 + region: content + settings: { } + third_party_settings: { } + field_typed_link: + type: typed_link + weight: 126 + region: content + settings: + placeholder_url: '' + placeholder_title: '' + third_party_settings: { } + langcode: + type: language_select + weight: 2 + region: content + settings: + include_locked: true + third_party_settings: { } + moderation_state: + type: moderation_state_default + weight: 100 + region: content + settings: { } + third_party_settings: { } + path: + type: path + weight: 30 + region: content + settings: { } + third_party_settings: { } + promote: + type: boolean_checkbox + weight: 15 + region: content + settings: + display_label: true + third_party_settings: { } + status: + type: boolean_checkbox + weight: 120 + region: content + settings: + display_label: true + third_party_settings: { } + sticky: + type: boolean_checkbox + weight: 16 + region: content + settings: + display_label: true + third_party_settings: { } + title: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } +hidden: { } diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_view_display.node.multivalue.default.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_view_display.node.multivalue.default.yml new file mode 100644 index 00000000..21f71e18 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_view_display.node.multivalue.default.yml @@ -0,0 +1,110 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.multivalue.field_address + - field.field.node.multivalue.field_description_list + - field.field.node.multivalue.field_link + - field.field.node.multivalue.field_link_description + - field.field.node.multivalue.field_paragraphs + - field.field.node.multivalue.field_textfield + - field.field.node.multivalue.field_timeline + - field.field.node.multivalue.field_typed_link + - node.type.multivalue + module: + - address + - description_list_field + - entity_reference_revisions + - link + - link_description + - oe_content_timeline_field + - typed_link + - user +id: node.multivalue.default +targetEntityType: node +bundle: multivalue +mode: default +content: + field_address: + type: address_default + label: above + settings: { } + third_party_settings: { } + weight: 102 + region: content + field_description_list: + type: description_list_formatter + label: above + settings: { } + third_party_settings: { } + weight: 103 + region: content + field_link: + type: link + label: above + settings: + trim_length: 80 + url_only: false + url_plain: false + rel: '' + target: '' + third_party_settings: { } + weight: 105 + region: content + field_link_description: + type: link_description + label: above + settings: + trim_length: 80 + url_only: false + url_plain: false + rel: '' + target: '' + third_party_settings: { } + weight: 104 + region: content + field_paragraphs: + type: entity_reference_revisions_entity_view + label: above + settings: + view_mode: default + link: '' + third_party_settings: { } + weight: 108 + region: content + field_textfield: + type: string + label: above + settings: + link_to_entity: false + third_party_settings: { } + weight: 101 + region: content + field_timeline: + type: timeline_formatter + label: above + settings: + limit: '0' + show_more: 'Show full timeline' + third_party_settings: { } + weight: 107 + region: content + field_typed_link: + type: typed_link + label: above + settings: + trim_length: 80 + url_only: false + url_plain: false + rel: '' + target: '' + third_party_settings: { } + weight: 106 + region: content + links: + settings: { } + third_party_settings: { } + weight: 100 + region: content +hidden: + langcode: true diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_view_display.paragraph.multivalue_paragraph.default.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_view_display.paragraph.multivalue_paragraph.default.yml new file mode 100644 index 00000000..876c5b65 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/core.entity_view_display.paragraph.multivalue_paragraph.default.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - field.field.paragraph.multivalue_paragraph.field_textfield_paragraph + - paragraphs.paragraphs_type.multivalue_paragraph +id: paragraph.multivalue_paragraph.default +targetEntityType: paragraph +bundle: multivalue_paragraph +mode: default +content: + field_textfield_paragraph: + type: string + label: above + settings: + link_to_entity: false + third_party_settings: { } + weight: 0 + region: content +hidden: { } diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_address.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_address.yml new file mode 100644 index 00000000..83db946f --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_address.yml @@ -0,0 +1,41 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_address + - node.type.multivalue + module: + - address + - content_translation +third_party_settings: + content_translation: + translation_sync: + langcode: langcode + country_code: country_code + administrative_area: administrative_area + locality: locality + dependent_locality: dependent_locality + postal_code: postal_code + sorting_code: sorting_code + address_line1: address_line1 + address_line2: address_line2 + organization: organization + given_name: given_name + additional_name: additional_name + family_name: family_name +id: node.multivalue.field_address +field_name: field_address +entity_type: node +bundle: multivalue +label: Address +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + available_countries: { } + langcode_override: '' + field_overrides: { } + fields: { } +field_type: address diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_description_list.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_description_list.yml new file mode 100644 index 00000000..46a24263 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_description_list.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_description_list + - node.type.multivalue + module: + - description_list_field +id: node.multivalue.field_description_list +field_name: field_description_list +entity_type: node +bundle: multivalue +label: 'Description list' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: description_list_field diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_link.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_link.yml new file mode 100644 index 00000000..8a2bfd17 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_link.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_link + - node.type.multivalue + module: + - link +id: node.multivalue.field_link +field_name: field_link +entity_type: node +bundle: multivalue +label: Link +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + title: 1 + link_type: 17 +field_type: link diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_link_description.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_link_description.yml new file mode 100644 index 00000000..799323fc --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_link_description.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_link_description + - node.type.multivalue + module: + - link_description +id: node.multivalue.field_link_description +field_name: field_link_description +entity_type: node +bundle: multivalue +label: 'Link description' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + title: 1 + link_type: 17 +field_type: link_description diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_paragraphs.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_paragraphs.yml new file mode 100644 index 00000000..5b0ba309 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_paragraphs.yml @@ -0,0 +1,30 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_paragraphs + - node.type.multivalue + - paragraphs.paragraphs_type.multivalue_paragraph + module: + - entity_reference_revisions +id: node.multivalue.field_paragraphs +field_name: field_paragraphs +entity_type: node +bundle: multivalue +label: Paragraphs +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:paragraph' + handler_settings: + target_bundles: + multivalue_paragraph: multivalue_paragraph + negate: 0 + target_bundles_drag_drop: + multivalue_paragraph: + weight: 2 + enabled: true +field_type: entity_reference_revisions diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_textfield.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_textfield.yml new file mode 100644 index 00000000..9cf49d35 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_textfield.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_textfield + - node.type.multivalue +id: node.multivalue.field_textfield +field_name: field_textfield +entity_type: node +bundle: multivalue +label: Textfield +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_timeline.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_timeline.yml new file mode 100644 index 00000000..c926eee9 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_timeline.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_timeline + - node.type.multivalue + module: + - oe_content_timeline_field +id: node.multivalue.field_timeline +field_name: field_timeline +entity_type: node +bundle: multivalue +label: Timeline +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: timeline_field diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_typed_link.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_typed_link.yml new file mode 100644 index 00000000..e5a067b7 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.node.multivalue.field_typed_link.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_typed_link + - node.type.multivalue + module: + - typed_link +id: node.multivalue.field_typed_link +field_name: field_typed_link +entity_type: node +bundle: multivalue +label: 'Typed Link' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + title: 1 + link_type: 17 +field_type: typed_link diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.paragraph.multivalue_paragraph.field_textfield_paragraph.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.paragraph.multivalue_paragraph.field_textfield_paragraph.yml new file mode 100644 index 00000000..aa7f0b01 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.field.paragraph.multivalue_paragraph.field_textfield_paragraph.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.paragraph.field_textfield_paragraph + - paragraphs.paragraphs_type.multivalue_paragraph +id: paragraph.multivalue_paragraph.field_textfield_paragraph +field_name: field_textfield_paragraph +entity_type: paragraph +bundle: multivalue_paragraph +label: 'Textfield Paragraph' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_address.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_address.yml new file mode 100644 index 00000000..beb9add6 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_address.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - address + - node +id: node.field_address +field_name: field_address +entity_type: node +type: address +settings: + translation_multivalue: false +module: address +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_description_list.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_description_list.yml new file mode 100644 index 00000000..32c784b1 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_description_list.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - description_list_field + - node +id: node.field_description_list +field_name: field_description_list +entity_type: node +type: description_list_field +settings: + translation_multivalue: false +module: description_list_field +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_link.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_link.yml new file mode 100644 index 00000000..759dc92c --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_link.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - link + - node +id: node.field_link +field_name: field_link +entity_type: node +type: link +settings: + translation_multivalue: false +module: link +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_link_description.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_link_description.yml new file mode 100644 index 00000000..58087766 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_link_description.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - link_description + - node +id: node.field_link_description +field_name: field_link_description +entity_type: node +type: link_description +settings: + translation_multivalue: false +module: link_description +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_paragraphs.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_paragraphs.yml new file mode 100644 index 00000000..332d836f --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_paragraphs.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - entity_reference_revisions + - node + - paragraphs +id: node.field_paragraphs +field_name: field_paragraphs +entity_type: node +type: entity_reference_revisions +settings: + target_type: paragraph +module: entity_reference_revisions +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_textfield.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_textfield.yml new file mode 100644 index 00000000..f06117b6 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_textfield.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.field_textfield +field_name: field_textfield +entity_type: node +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false + translation_multivalue: false +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_timeline.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_timeline.yml new file mode 100644 index 00000000..20f2a4a5 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_timeline.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - node + - oe_content_timeline_field +id: node.field_timeline +field_name: field_timeline +entity_type: node +type: timeline_field +settings: + translation_multivalue: false +module: oe_content_timeline_field +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_typed_link.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_typed_link.yml new file mode 100644 index 00000000..201f98f3 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.node.field_typed_link.yml @@ -0,0 +1,23 @@ +langcode: en +status: true +dependencies: + module: + - node + - typed_link +id: node.field_typed_link +field_name: field_typed_link +entity_type: node +type: typed_link +settings: + allowed_values: + value_one: 'Value one' + value_two: 'Value two' + allowed_values_function: '' + translation_multivalue: false +module: typed_link +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.paragraph.field_textfield_paragraph.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.paragraph.field_textfield_paragraph.yml new file mode 100644 index 00000000..1450c62c --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/field.storage.paragraph.field_textfield_paragraph.yml @@ -0,0 +1,21 @@ +langcode: en +status: true +dependencies: + module: + - paragraphs +id: paragraph.field_textfield_paragraph +field_name: field_textfield_paragraph +entity_type: paragraph +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false + translation_multivalue: false +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/language.content_settings.node.multivalue.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/language.content_settings.node.multivalue.yml new file mode 100644 index 00000000..d6045bd2 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/language.content_settings.node.multivalue.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + config: + - node.type.multivalue + module: + - content_translation +third_party_settings: + content_translation: + enabled: true + bundle_settings: + untranslatable_fields_hide: '0' +id: node.multivalue +target_entity_type_id: node +target_bundle: multivalue +default_langcode: site_default +language_alterable: true diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/language.content_settings.paragraph.multivalue_paragraph.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/language.content_settings.paragraph.multivalue_paragraph.yml new file mode 100644 index 00000000..21c91183 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/language.content_settings.paragraph.multivalue_paragraph.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + config: + - paragraphs.paragraphs_type.multivalue_paragraph + module: + - content_translation +third_party_settings: + content_translation: + enabled: true + bundle_settings: + untranslatable_fields_hide: '0' +id: paragraph.multivalue_paragraph +target_entity_type_id: paragraph +target_bundle: multivalue_paragraph +default_langcode: site_default +language_alterable: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/node.type.multivalue.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/node.type.multivalue.yml new file mode 100644 index 00000000..0d1b5504 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/node.type.multivalue.yml @@ -0,0 +1,16 @@ +langcode: en +status: true +dependencies: + module: + - menu_ui +third_party_settings: + menu_ui: + available_menus: { } + parent: '' +name: Multivalue +type: multivalue +description: '' +help: '' +new_revision: true +preview_mode: 0 +display_submitted: false diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/paragraphs.paragraphs_type.multivalue_paragraph.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/paragraphs.paragraphs_type.multivalue_paragraph.yml new file mode 100644 index 00000000..afb19e9b --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/config/install/paragraphs.paragraphs_type.multivalue_paragraph.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: { } +id: multivalue_paragraph +label: 'Multivalue paragraph' +icon_uuid: null +icon_default: null +description: '' +behavior_plugins: { } diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/oe_translation_multivalue_test.info.yml b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/oe_translation_multivalue_test.info.yml new file mode 100644 index 00000000..0f19f18c --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/modules/oe_translation_multivalue_test/oe_translation_multivalue_test.info.yml @@ -0,0 +1,15 @@ +name: OE Translation Multivalue Test +description: Test module for OE Translation Multivalue +core_version_requirement: ^9.4 || ^10 +type: module +package: Testing + +dependencies: + - oe_translation:oe_translation_local + - address:address + - paragraphs:paragraphs + - typed_link:typed_link + - oe_content_timeline_field:oe_content_timeline_field + - description_list_field:description_list_field + - link_description:link_description + - drupal:options diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/src/Functional/MultivalueTranslationsTest.php b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/src/Functional/MultivalueTranslationsTest.php new file mode 100644 index 00000000..2aac4535 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/src/Functional/MultivalueTranslationsTest.php @@ -0,0 +1,507 @@ +schema(); + foreach ($field_names as $field_name) { + $tables = []; + foreach (['node__', 'node_revision__'] as $prefix) { + $tables[] = $prefix . $field_name; + } + + // Assert the table doesn't yet have the column. + foreach ($tables as $table) { + $this->assertFalse($schema->fieldExists($table, $field_name . '_translation_id')); + } + $field_definitions = $entity_field_manager->getFieldStorageDefinitions('node'); + $field_definition = $field_definitions[$field_name]; + $this->assertFalse(in_array('translation_id', $field_definition->getPropertyNames())); + + $storage = FieldStorageConfig::load('node.' . $field_name); + $storage->setSetting('translation_multivalue', TRUE); + $storage->save(); + + // All the fields are already multivalue, so the column should now be + // created. + foreach ($tables as $table) { + $this->assertTrue($schema->fieldExists($table, $field_name . '_translation_id')); + } + $entity_field_manager->clearCachedFieldDefinitions(); + $field_definitions = $entity_field_manager->getFieldStorageDefinitions('node'); + $field_definition = $field_definitions[$field_name]; + $this->assertTrue(in_array('translation_id', $field_definition->getPropertyNames())); + + // Change the cardinality to 1 and assert we no longer have the column. + $storage = FieldStorageConfig::load('node.' . $field_name); + $storage->setCardinality(1); + $storage->save(); + + foreach ($tables as $table) { + $this->assertFalse($schema->fieldExists($table, $field_name . '_translation_id')); + } + + $entity_field_manager->clearCachedFieldDefinitions(); + $field_definitions = $entity_field_manager->getFieldStorageDefinitions('node'); + $field_definition = $field_definitions[$field_name]; + $this->assertFalse(in_array('translation_id', $field_definition->getPropertyNames())); + } + } + + /** + * Tests the translation of multivalue fields using the API. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function testMultivalueFieldTranslationsApi(): void { + // Enable the multivalue translation on our fields. + $field_names = [ + 'field_textfield', + 'field_address', + 'field_description_list', + 'field_link', + 'field_link_description', + 'field_timeline', + 'field_typed_link', + ]; + + foreach ($field_names as $field_name) { + $storage = FieldStorageConfig::load('node.' . $field_name); + $storage->setSetting('translation_multivalue', TRUE); + $storage->save(); + } + $storage = FieldStorageConfig::load('paragraph.field_textfield_paragraph'); + $storage->setSetting('translation_multivalue', TRUE); + $storage->save(); + + // Create a node with all the values, each field with 2 deltas. + $paragraph = Paragraph::create([ + 'type' => 'multivalue_paragraph', + 'field_textfield_paragraph' => [ + 'Value 1', + 'Value 2', + ], + ]); + $paragraph->save(); + + $node = Node::create([ + 'type' => 'multivalue', + 'title' => 'Translation node', + 'field_paragraphs' => [ + [ + 'target_id' => $paragraph->id(), + 'target_revision_id' => $paragraph->getRevisionId(), + ], + ], + 'field_textfield' => [ + 'Value 1', + 'Value 2', + ], + 'field_address' => [ + [ + 'country_code' => 'BE', + 'given_name' => 'The first name 1', + 'family_name' => 'The last name 1', + 'locality' => 'Brussels', + 'postal_code' => '1000', + 'address_line1' => 'The street name 1', + ], + [ + 'country_code' => 'BE', + 'given_name' => 'The first name 2', + 'family_name' => 'The last name 2', + 'locality' => 'Brussels', + 'postal_code' => '1000', + 'address_line1' => 'The street name 2', + ], + ], + 'field_description_list' => [ + [ + 'term' => 'term one', + 'description' => 'Description 1', + 'format' => 'plain_text', + ], + [ + 'term' => 'term two', + 'description' => 'Description 2', + 'format' => 'plain_text', + ], + ], + 'field_link_description' => [ + [ + 'uri' => 'http://example.com/one', + 'description' => 'Description 1', + ], + [ + 'uri' => 'http://example.com/two', + 'description' => 'Description 2', + ], + ], + 'field_link' => [ + [ + 'uri' => 'http://example.com/one', + ], + [ + 'uri' => 'http://example.com/two', + ], + ], + 'field_timeline' => [ + [ + 'label' => 'label one', + 'title' => 'title one', + 'body' => 'body one', + 'format' => 'plain_text', + ], + [ + 'label' => 'label two', + 'title' => 'title two', + 'body' => 'body two', + 'format' => 'plain_text', + ], + ], + 'field_typed_link' => [ + [ + 'uri' => 'http://example.com/one', + 'link_type' => 'value_one', + ], + [ + 'uri' => 'http://example.com/two', + 'link_type' => 'value_two', + ], + ], + ]); + + $node->save(); + $node = $this->drupalGetNodeByTitle('Translation node', TRUE); + + // Start to keep track of the translation IDs that were created for each + // of the values. + $translation_ids = []; + foreach ($field_names as $field_name) { + $values = $node->get($field_name)->getValue(); + foreach ($values as $value) { + $translation_ids[$field_name]['en'][$value['translation_id']] = $value; + } + } + $values = $node->get('field_paragraphs')->entity->get('field_textfield_paragraph')->getValue(); + foreach ($values as $value) { + $translation_ids['field_textfield_paragraph']['en'][$value['translation_id']] = $value; + } + + $this->drupalGet($node->toUrl()); + + // Translate the node. + $this->clickLink('Translate'); + $this->clickLink('Local translations'); + $this->getSession()->getPage()->find('css', 'table tbody tr[hreflang="fr"] a')->click(); + + // Translate the values. + $tables = $this->getSession()->getPage()->findAll('css', 'table.responsive-enabled'); + foreach ($tables as $table) { + $textarea = $table->find('xpath', '//td[2]//textarea'); + $value = $textarea->getValue(); + if (in_array($value, ['BE', 'Brussels', '1000'])) { + continue; + } + $textarea->setValue($value . '/FR'); + } + $this->getSession()->getPage()->pressButton('Save and synchronise'); + $node = $this->drupalGetNodeByTitle('Translation node', TRUE); + $this->assertTrue($node->hasTranslation('fr')); + + $translation = $node->getTranslation('fr'); + foreach ($field_names as $field_name) { + $values = $translation->get($field_name)->getValue(); + foreach ($values as $value) { + $translation_ids[$field_name]['fr'][$value['translation_id']] = $value; + } + } + \Drupal::entityTypeManager()->getStorage('paragraph')->resetCache(); + $values = $node->get('field_paragraphs')->entity->getTranslation('fr')->get('field_textfield_paragraph')->getValue(); + foreach ($values as $value) { + $translation_ids['field_textfield_paragraph']['fr'][$value['translation_id']] = $value; + } + + // Assert that the translation IDs are the same for both the EN and FR + // deltas. + foreach ($translation_ids as $field_name => $data) { + $this->assertEquals(array_keys($data['en']), array_keys($data['fr'])); + } + + // Reorder the values on all the fields. + $paragraph = $node->get('field_paragraphs')->entity; + $paragraph->setNewRevision(); + $values = $paragraph->get('field_textfield_paragraph')->getValue(); + $values = array_reverse($values); + $paragraph->set('field_textfield_paragraph', $values); + $paragraph->save(); + foreach ($field_names as $field_name) { + $values = $node->get($field_name)->getValue(); + $values = array_reverse($values); + $node->set($field_name, $values); + $node->set('field_paragraphs', [ + 'target_id' => $paragraph->id(), + 'target_revision_id' => $paragraph->getRevisionId(), + ]); + $node->setNewRevision(); + $node->save(); + } + + $this->drupalGet($node->toUrl()); + + $node = $this->drupalGetNodeByTitle('Translation node', TRUE); + $new_translation_ids = $this->createTranslationIdMap($node, $field_names); + + // Build an array of array keys to use for asserting the value matches. + $value_test_keys = [ + 'field_textfield' => 'value', + 'field_address' => 'address_line1', + 'field_description_list' => 'term', + 'field_link' => 'uri', + 'field_link_description' => 'uri', + 'field_timeline' => 'label', + 'field_typed_link' => 'uri', + ]; + + // Assert that now that we have reordered each field, the deltas are no + // longer matching, but the translation IDs still are representative of + // the connection between the original value and the translation. + foreach ($field_names as $field_name) { + // First we assert the expectation that the EN delta 0's translation + // ID is now matching the delta 1's translation ID because the translation + // hasn't yet been updated to reflect the change in delta. + $this->assertNotNull($new_translation_ids[$field_name]['en'][0]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['en'][1]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['fr'][0]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['fr'][1]['translation_id']); + $this->assertEquals($new_translation_ids[$field_name]['en'][0]['translation_id'], $new_translation_ids[$field_name]['fr'][1]['translation_id']); + $this->assertEquals($new_translation_ids[$field_name]['en'][1]['translation_id'], $new_translation_ids[$field_name]['fr'][0]['translation_id']); + + // Next, assert that the values are mapped by the translation IDs (just + // like above, with the deltas not matching). + $key = $value_test_keys[$field_name]; + $this->assertEquals($new_translation_ids[$field_name]['en'][0][$key] . '/FR', $new_translation_ids[$field_name]['fr'][1][$key]); + $this->assertEquals($new_translation_ids[$field_name]['en'][1][$key] . '/FR', $new_translation_ids[$field_name]['fr'][0][$key]); + } + + // Translate the node again and assert that the translation values are + // correctly pre-filled, not based on the delta, but based on the matching + // translation IDs. + $this->drupalGet($node->toUrl()); + $this->clickLink('Translate'); + $this->clickLink('Local translations'); + $this->getSession()->getPage()->find('css', 'table tbody tr[hreflang="fr"] a')->click(); + foreach ($field_names as $field_name) { + $suffix = ''; + $key = $value_test_keys[$field_name]; + foreach ([0, 1] as $delta) { + $source = $this->getSession()->getPage()->find('xpath', sprintf('//table//td//textarea[@name="%s|%s|%s[source]%s"]', $field_name, $delta, $key, $suffix))->getValue(); + $translation = $this->getSession()->getPage()->find('xpath', sprintf('//table//td//textarea[@name="%s|%s|%s[translation]%s"]', $field_name, $delta, $key, $suffix))->getValue(); + $this->assertEquals($source . '/FR', $translation); + } + } + + // Save the translation. + $this->getSession()->getPage()->pressButton('Save and synchronise'); + $node = $this->drupalGetNodeByTitle('Translation node', TRUE); + + // Assert that now the deltas are in line with the values and the + // translation IDs. + $new_translation_ids = $this->createTranslationIdMap($node, $field_names); + + foreach ($field_names as $field_name) { + $this->assertNotNull($new_translation_ids[$field_name]['en'][0]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['en'][1]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['fr'][0]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['fr'][1]['translation_id']); + $this->assertEquals($new_translation_ids[$field_name]['en'][0]['translation_id'], $new_translation_ids[$field_name]['fr'][0]['translation_id']); + $this->assertEquals($new_translation_ids[$field_name]['en'][1]['translation_id'], $new_translation_ids[$field_name]['fr'][1]['translation_id']); + $key = $value_test_keys[$field_name]; + $this->assertEquals($new_translation_ids[$field_name]['en'][0][$key] . '/FR', $new_translation_ids[$field_name]['fr'][0][$key]); + $this->assertEquals($new_translation_ids[$field_name]['en'][1][$key] . '/FR', $new_translation_ids[$field_name]['fr'][1][$key]); + } + + // Start a new translation and assert the values are pre-filled correctly + // still. + $this->drupalGet($node->toUrl()); + $this->clickLink('Translate'); + $this->clickLink('Local translations'); + $this->getSession()->getPage()->find('css', 'table tbody tr[hreflang="fr"] a')->click(); + foreach ($field_names as $field_name) { + $suffix = ''; + $key = $value_test_keys[$field_name]; + foreach ([0, 1] as $delta) { + $source = $this->getSession()->getPage()->find('xpath', sprintf('//table//td//textarea[@name="%s|%s|%s[source]%s"]', $field_name, $delta, $key, $suffix))->getValue(); + $translation = $this->getSession()->getPage()->find('xpath', sprintf('//table//td//textarea[@name="%s|%s|%s[translation]%s"]', $field_name, $delta, $key, $suffix))->getValue(); + $this->assertEquals($source . '/FR', $translation); + } + } + + $new_translation_ids = $this->createTranslationIdMap($node, $field_names); + foreach ($field_names as $field_name) { + $this->assertNotNull($new_translation_ids[$field_name]['en'][0]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['en'][1]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['fr'][0]['translation_id']); + $this->assertNotNull($new_translation_ids[$field_name]['fr'][1]['translation_id']); + $this->assertEquals($new_translation_ids[$field_name]['en'][0]['translation_id'], $new_translation_ids[$field_name]['fr'][0]['translation_id']); + $this->assertEquals($new_translation_ids[$field_name]['en'][1]['translation_id'], $new_translation_ids[$field_name]['fr'][1]['translation_id']); + $key = $value_test_keys[$field_name]; + $this->assertEquals($new_translation_ids[$field_name]['en'][0][$key] . '/FR', $new_translation_ids[$field_name]['fr'][0][$key]); + $this->assertEquals($new_translation_ids[$field_name]['en'][1][$key] . '/FR', $new_translation_ids[$field_name]['fr'][1][$key]); + } + } + + /** + * Tests that we can update the configuration of fields that contain data. + */ + public function testFieldConfigurationUpdate(): void { + $entity_field_manager = \Drupal::service('entity_field.manager'); + + $node = Node::create([ + 'type' => 'multivalue', + 'title' => 'Translation node', + 'field_textfield' => [ + 'Value 1', + 'Value 2', + ], + ]); + $node->save(); + + $database = \Drupal::database(); + $schema = $database->schema(); + $tables = []; + $field_name = 'field_textfield'; + foreach (['node__', 'node_revision__'] as $prefix) { + $tables[] = $prefix . $field_name; + } + + // Assert the table doesn't yet have the column but there are values in the + // tables. + foreach ($tables as $table) { + $this->assertFalse($schema->fieldExists($table, $field_name . '_translation_id')); + $this->assertNotEmpty($database->select($table)->fields($table)->execute()->fetchAll()); + } + $field_definitions = $entity_field_manager->getFieldStorageDefinitions('node'); + $field_definition = $field_definitions[$field_name]; + $this->assertFalse(in_array('translation_id', $field_definition->getPropertyNames())); + + // Install the field column. + TranslationMultivalueColumnInstaller::installColumn('node.field_textfield'); + + // Assert that we have the column, the field definition update and the + // content is still in the field tables. + foreach ($tables as $table) { + $this->assertTrue($schema->fieldExists($table, $field_name . '_translation_id')); + $this->assertNotEmpty($database->select($table)->fields($table)->execute()->fetchAll()); + } + $entity_field_manager->clearCachedFieldDefinitions(); + $field_definitions = $entity_field_manager->getFieldStorageDefinitions('node'); + $field_definition = $field_definitions[$field_name]; + $this->assertTrue(in_array('translation_id', $field_definition->getPropertyNames())); + $node = $this->drupalGetNodeByTitle('Translation node', TRUE); + $this->assertEquals([ + [ + 'value' => 'Value 1', + 'translation_id' => NULL, + ], + [ + 'value' => 'Value 2', + 'translation_id' => NULL, + ], + ], $node->get('field_textfield')->getValue()); + } + + /** + * Creates an array of the field values and their translation IDs. + * + * @param \Drupal\node\NodeInterface $node + * The node. + * @param array $field_names + * The field names. + * + * @return array + * The value map. + */ + protected function createTranslationIdMap(NodeInterface $node, array $field_names): array { + $translation_ids = []; + foreach ($field_names as $field_name) { + $values = $node->get($field_name)->getValue(); + foreach ($values as $value) { + $translation_ids[$field_name]['en'][] = $value; + } + } + $values = $node->get('field_paragraphs')->entity->get('field_textfield_paragraph')->getValue(); + foreach ($values as $value) { + $translation_ids['field_textfield_paragraph']['en'][] = $value; + } + $translation = $node->getTranslation('fr'); + foreach ($field_names as $field_name) { + $values = $translation->get($field_name)->getValue(); + foreach ($values as $value) { + $translation_ids[$field_name]['fr'][] = $value; + } + } + \Drupal::entityTypeManager()->getStorage('paragraph')->resetCache(); + $values = $node->get('field_paragraphs')->entity->getTranslation('fr')->get('field_textfield_paragraph')->getValue(); + foreach ($values as $value) { + $translation_ids['field_textfield_paragraph']['fr'] = $value; + } + + return $translation_ids; + } + +} diff --git a/modules/oe_translation_local/modules/oe_translation_multivalue/tests/src/FunctionalJavascript/MultivalueTranslationsJsTest.php b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/src/FunctionalJavascript/MultivalueTranslationsJsTest.php new file mode 100644 index 00000000..f9297603 --- /dev/null +++ b/modules/oe_translation_local/modules/oe_translation_multivalue/tests/src/FunctionalJavascript/MultivalueTranslationsJsTest.php @@ -0,0 +1,170 @@ +setSetting('translation_multivalue', TRUE); + $storage->save(); + } + $storage = FieldStorageConfig::load('paragraph.field_textfield_paragraph'); + $storage->setSetting('translation_multivalue', TRUE); + $storage->save(); + + $user = $this->createUser([], NULL, TRUE); + $this->drupalLogin($user); + $this->drupalGet('/node/add/multivalue'); + $this->getSession()->getPage()->fillField('Title', 'Test title'); + // Textfield. + $this->getSession()->getPage()->fillField('field_textfield[0][value]', 'Value 1'); + $this->getSession()->getPage()->pressButton('field_textfield_add_more'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->getSession()->getPage()->fillField('field_textfield[1][value]', 'Value 2'); + // Address. + $this->getSession()->getPage()->selectFieldOption('field_address[0][address][country_code]', 'BE'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->getSession()->getPage()->fillField('field_address[0][address][given_name]', 'First name 1'); + $this->getSession()->getPage()->fillField('field_address[0][address][family_name]', 'Last name 1'); + $this->getSession()->getPage()->fillField('field_address[0][address][address_line1]', 'Street 1'); + $this->getSession()->getPage()->fillField('field_address[0][address][postal_code]', '1000'); + $this->getSession()->getPage()->fillField('field_address[0][address][locality]', 'Brussels'); + $this->getSession()->getPage()->pressButton('field_address_add_more'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->getSession()->getPage()->selectFieldOption('field_address[1][address][country_code]', 'BE'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->getSession()->getPage()->fillField('field_address[1][address][given_name]', 'First name 2'); + $this->getSession()->getPage()->fillField('field_address[1][address][family_name]', 'Last name 2'); + $this->getSession()->getPage()->fillField('field_address[1][address][address_line1]', 'Street 2'); + $this->getSession()->getPage()->fillField('field_address[1][address][postal_code]', '1000'); + $this->getSession()->getPage()->fillField('field_address[1][address][locality]', 'Brussels'); + $this->getSession()->executeScript('window.scrollTo(0,0);'); + $this->getSession()->getPage()->pressButton('Save'); + $this->assertSession()->pageTextContains('Multivalue Test title has been created.'); + + // Translate the node. + $node = $this->drupalGetNodeByTitle('Test title'); + $this->drupalGet($node->toUrl()); + $this->clickLink('Translate'); + $this->clickLink('Local translations'); + $this->getSession()->getPage()->find('css', 'table tbody tr[hreflang="fr"] a')->click(); + + // Translate the values. + $tables = $this->getSession()->getPage()->findAll('css', 'table.responsive-enabled'); + foreach ($tables as $table) { + $textarea = $table->find('xpath', '//td[2]//textarea'); + $value = $textarea->getValue(); + if (in_array($value, ['BE', 'Brussels', '1000'])) { + continue; + } + $textarea->setValue($value . '/FR'); + } + $this->getSession()->getPage()->pressButton('Save and synchronise'); + $node = $this->drupalGetNodeByTitle('Test title', TRUE); + $this->assertTrue($node->hasTranslation('fr')); + + // Reorder the values. + $this->drupalGet($node->toUrl('edit-form')); + $this->getSession()->getPage()->find('css', '.field--name-field-textfield')->pressButton('Show row weights'); + $this->assertSession()->waitForElementVisible('css', '[name="field_textfield[0][_weight]"]'); + $this->getSession()->getPage()->selectFieldOption('field_textfield[0][_weight]', 1); + $this->getSession()->getPage()->selectFieldOption('field_textfield[1][_weight]', 0); + $this->getSession()->getPage()->selectFieldOption('field_address[0][_weight]', 1); + $this->getSession()->getPage()->selectFieldOption('field_address[1][_weight]', 0); + $this->getSession()->executeScript('window.scrollTo(0,0);'); + $this->getSession()->getPage()->pressButton('Save'); + $this->assertSession()->pageTextContains('Multivalue Test title has been updated.'); + // Assert the order change took effect. + $node = $this->drupalGetNodeByTitle('Test title', TRUE); + $this->assertEquals('Value 2', $node->get('field_textfield')->getValue()[0]['value']); + $this->assertEquals('Value 1', $node->get('field_textfield')->getValue()[1]['value']); + $this->assertEquals('Street 2', $node->get('field_address')->getValue()[0]['address_line1']); + $this->assertEquals('Street 1', $node->get('field_address')->getValue()[1]['address_line1']); + // The translation value are the other way. + $translation = $node->getTranslation('fr'); + $this->assertEquals('Value 1/FR', $translation->get('field_textfield')->getValue()[0]['value']); + $this->assertEquals('Value 2/FR', $translation->get('field_textfield')->getValue()[1]['value']); + $this->assertEquals('Street 1/FR', $translation->get('field_address')->getValue()[0]['address_line1']); + $this->assertEquals('Street 2/FR', $translation->get('field_address')->getValue()[1]['address_line1']); + + // Translate again the node and assert the pre-filled translation values + // match the source values. + $this->clickLink('Translate'); + $this->clickLink('Local translations'); + $this->getSession()->getPage()->find('css', 'table tbody tr[hreflang="fr"] a')->click(); + + $value_test_keys = [ + 'field_textfield' => 'value', + 'field_address' => 'address_line1', + ]; + foreach ($field_names as $field_name) { + $key = $value_test_keys[$field_name]; + foreach ([0, 1] as $delta) { + $source = $this->getSession()->getPage()->find('xpath', sprintf('//table//td//textarea[@name="%s|%s|%s[source]"]', $field_name, $delta, $key))->getValue(); + $translation = $this->getSession()->getPage()->find('xpath', sprintf('//table//td//textarea[@name="%s|%s|%s[translation]"]', $field_name, $delta, $key))->getValue(); + $this->assertEquals($source . '/FR', $translation); + } + } + // Save the translation. + $this->getSession()->getPage()->pressButton('Save and synchronise'); + $node = $this->drupalGetNodeByTitle('Test title', TRUE); + $this->assertEquals('Value 2', $node->get('field_textfield')->getValue()[0]['value']); + $this->assertEquals('Value 1', $node->get('field_textfield')->getValue()[1]['value']); + $this->assertEquals('Street 2', $node->get('field_address')->getValue()[0]['address_line1']); + $this->assertEquals('Street 1', $node->get('field_address')->getValue()[1]['address_line1']); + // The translation value are now in the same order as the source. + $translation = $node->getTranslation('fr'); + $this->assertEquals('Value 2/FR', $translation->get('field_textfield')->getValue()[0]['value']); + $this->assertEquals('Value 1/FR', $translation->get('field_textfield')->getValue()[1]['value']); + $this->assertEquals('Street 2/FR', $translation->get('field_address')->getValue()[0]['address_line1']); + $this->assertEquals('Street 1/FR', $translation->get('field_address')->getValue()[1]['address_line1']); + } + +} diff --git a/modules/oe_translation_local/src/Form/LocalTranslationRequestForm.php b/modules/oe_translation_local/src/Form/LocalTranslationRequestForm.php index e94f2db5..fda2bd31 100644 --- a/modules/oe_translation_local/src/Form/LocalTranslationRequestForm.php +++ b/modules/oe_translation_local/src/Form/LocalTranslationRequestForm.php @@ -101,9 +101,7 @@ public function form(array $form, FormStateInterface $form_state) { // translation values. $data = $translation_request->getData(); if (!$this->hasAnyTranslations($data)) { - $entity = $translation_request->getContentEntity(); - $existing_translation = $entity->hasTranslation($langcode) ? $entity->getTranslation($langcode) : NULL; - $existing_translation_data = $existing_translation ? $this->translationSourceManager->extractData($existing_translation) : []; + $existing_translation_data = $this->getExistingTranslationData($translation_request, $langcode); } // Disable the element if the translation request is accepted. @@ -354,4 +352,15 @@ protected function hasAnyTranslations(array $data): bool { return FALSE; } + /** + * Creates and returns the existing translation data. + */ + protected function getExistingTranslationData(TranslationRequestLocal $translation_request, string $langcode): array { + $entity = $translation_request->getContentEntity(); + $existing_translation = $entity->hasTranslation($langcode) ? $entity->getTranslation($langcode) : NULL; + $existing_translation_data = $existing_translation ? $this->translationSourceManager->extractData($existing_translation) : []; + + return $existing_translation_data; + } + } diff --git a/oe_translation.module b/oe_translation.module index ac41c3b4..01871e81 100644 --- a/oe_translation.module +++ b/oe_translation.module @@ -24,6 +24,7 @@ use Drupal\oe_translation\Entity\TranslationRequestLogInterface; use Drupal\oe_translation\TranslationModerationHandler; use Drupal\oe_translation\TranslationSourceFieldProcessor\AddressFieldProcessor; use Drupal\oe_translation\TranslationSourceFieldProcessor\DefaultFieldProcessor; +use Drupal\oe_translation\TranslationSourceFieldProcessor\DescriptionListFieldProcessor; use Drupal\oe_translation\TranslationSourceFieldProcessor\MetatagsFieldProcessor; use Drupal\oe_translation\TranslationSourceFieldProcessor\PathFieldProcessor; @@ -134,6 +135,9 @@ function oe_translation_field_info_alter(&$info) { if (isset($info['address'])) { $info['address']['oe_translation_source_field_processor'] = AddressFieldProcessor::class; } + if (isset($info['description_list_field'])) { + $info['description_list_field']['oe_translation_source_field_processor'] = DescriptionListFieldProcessor::class; + } if (isset($info['typed_link'])) { $info['typed_link']['column_groups'] = [ 'uri' => [ diff --git a/src/TranslationSourceFieldProcessor/DescriptionListFieldProcessor.php b/src/TranslationSourceFieldProcessor/DescriptionListFieldProcessor.php new file mode 100644 index 00000000..93be02b4 --- /dev/null +++ b/src/TranslationSourceFieldProcessor/DescriptionListFieldProcessor.php @@ -0,0 +1,34 @@ + &$value) { + if (!is_numeric($delta)) { + continue; + } + if (isset($value['term']['#format'])) { + unset($value['term']['#format']); + } + } + + return $data; + } + +}