entityManager = $entityManager; } /** * {@inheritdoc} */ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) { $translations = $entity->getTranslationLanguages(); $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); // If we have no information about what to sync to, if we are creating a new // entity, if we have no translations for the current entity and we are not // creating one, then there is nothing to synchronize. if (empty($sync_langcode) || $entity->isNew() || count($translations) < 2) { return; } // If the entity language is being changed there is nothing to synchronize. $entity_type = $entity->getEntityTypeId(); $entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorage($entity_type)->loadUnchanged($entity->id()); if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) { return; } /** @var \Drupal\Core\Field\FieldItemListInterface $items */ foreach ($entity as $field_name => $items) { $field_definition = $items->getFieldDefinition(); $field_type_definition = $field_type_manager->getDefinition($field_definition->getType()); $column_groups = $field_type_definition['column_groups']; // Sync if the field is translatable, not empty, and the synchronization // setting is enabled. if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable() && !$items->isEmpty() && $translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync')) { // Retrieve all the untranslatable column groups and merge them into // single list. $groups = array_keys(array_diff($translation_sync, array_filter($translation_sync))); // If a group was selected has the require_all_groups_for_translation // flag set, there are no untranslatable columns. This is done because // the UI adds Javascript that disables the other checkboxes, so their // values are not saved. foreach (array_filter($translation_sync) as $group) { if (!empty($column_groups[$group]['require_all_groups_for_translation'])) { $groups = []; break; } } if (!empty($groups)) { $columns = []; foreach ($groups as $group) { $info = $column_groups[$group]; // A missing 'columns' key indicates we have a single-column group. $columns = array_merge($columns, isset($info['columns']) ? $info['columns'] : [$group]); } if (!empty($columns)) { $values = []; foreach ($translations as $langcode => $language) { $values[$langcode] = $entity->getTranslation($langcode)->get($field_name)->getValue(); } // If a translation is being created, the original values should be // used as the unchanged items. In fact there are no unchanged items // to check against. $langcode = $original_langcode ?: $sync_langcode; $unchanged_items = $entity_unchanged->getTranslation($langcode)->get($field_name)->getValue(); $this->synchronizeItems($values, $unchanged_items, $sync_langcode, array_keys($translations), $columns); foreach ($translations as $langcode => $language) { $entity->getTranslation($langcode)->get($field_name)->setValue($values[$langcode]); } } } } } } /** * {@inheritdoc} */ public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $columns) { $source_items = $values[$sync_langcode]; // Make sure we can detect any change in the source items. $change_map = []; // By picking the maximum size between updated and unchanged items, we make // sure to process also removed items. $total = max([count($source_items), count($unchanged_items)]); // As a first step we build a map of the deltas corresponding to the column // values to be synchronized. Recording both the old values and the new // values will allow us to detect any change in the order of the new items // for each column. for ($delta = 0; $delta < $total; $delta++) { foreach (['old' => $unchanged_items, 'new' => $source_items] as $key => $items) { if ($item_id = $this->itemHash($items, $delta, $columns)) { $change_map[$item_id][$key][] = $delta; } } } // Backup field values and the change map. $original_field_values = $values; $original_change_map = $change_map; // Reset field values so that no spurious one is stored. Source values must // be preserved in any case. $values = [$sync_langcode => $source_items]; // Update field translations. foreach ($translations as $langcode) { // We need to synchronize only values different from the source ones. if ($langcode != $sync_langcode) { // Reinitialize the change map as it is emptied while processing each // language. $change_map = $original_change_map; // By using the maximum cardinality we ensure to process removed items. for ($delta = 0; $delta < $total; $delta++) { // By inspecting the map we built before we can tell whether a value // has been created or removed. A changed value will be interpreted as // a new value, in fact it did not exist before. $created = TRUE; $removed = TRUE; $old_delta = NULL; $new_delta = NULL; if ($item_id = $this->itemHash($source_items, $delta, $columns)) { if (!empty($change_map[$item_id]['old'])) { $old_delta = array_shift($change_map[$item_id]['old']); } if (!empty($change_map[$item_id]['new'])) { $new_delta = array_shift($change_map[$item_id]['new']); } $created = $created && !isset($old_delta); $removed = $removed && !isset($new_delta); } // If an item has been removed we do not store its translations. if ($removed) { continue; } // If a synchronized column has changed or has been created from // scratch we need to replace the values for this language as a // combination of the values that need to be synced from the source // items and the other columns from the existing values. This only // works if the delta exists in the language. elseif ($created && !empty($original_field_values[$langcode][$delta])) { $item_columns_to_sync = array_intersect_key($source_items[$delta], array_flip($columns)); $item_columns_to_keep = array_diff_key($original_field_values[$langcode][$delta], array_flip($columns)); $values[$langcode][$delta] = $item_columns_to_sync + $item_columns_to_keep; } // If the delta doesn't exist, copy from the source language. elseif ($created) { $values[$langcode][$delta] = $source_items[$delta]; } // Otherwise the current item might have been reordered. elseif (isset($old_delta) && isset($new_delta)) { // If for any reason the old value is not defined for the current // language we fall back to the new source value, this way we ensure // the new values are at least propagated to all the translations. // If the value has only been reordered we just move the old one in // the new position. $item = isset($original_field_values[$langcode][$old_delta]) ? $original_field_values[$langcode][$old_delta] : $source_items[$new_delta]; $values[$langcode][$new_delta] = $item; } } } } } /** * Computes a hash code for the specified item. * * @param array $items * An array of field items. * @param int $delta * The delta identifying the item to be processed. * @param array $columns * An array of column names to be synchronized. * * @returns string * A hash code that can be used to identify the item. */ protected function itemHash(array $items, $delta, array $columns) { $values = []; if (isset($items[$delta])) { foreach ($columns as $column) { if (!empty($items[$delta][$column])) { $value = $items[$delta][$column]; // String and integer values are by far the most common item values, // thus we special-case them to improve performance. $values[] = is_string($value) || is_int($value) ? $value : hash('sha256', serialize($value)); } else { // Explicitly track also empty values. $values[] = ''; } } } return implode('.', $values); } }