diff --git a/core/modules/config_translation/migration_templates/d6_i18n_user_profile_field_instance.yml b/core/modules/config_translation/migration_templates/d6_i18n_user_profile_field_instance.yml new file mode 100644 index 0000000..f2c6f95 --- /dev/null +++ b/core/modules/config_translation/migration_templates/d6_i18n_user_profile_field_instance.yml @@ -0,0 +1,29 @@ +id: d6_i18n_user_profile_field_instance +label: User profile field instance configuration +migration_tags: + - Drupal 6 +source: + plugin: d6_i18n_profile_field + constants: + entity_type: user + bundle: user +process: + langcode: language + entity_type: 'constants/entity_type' + bundle: 'constants/bundle' + field_name: name + property: + plugin: static_map + source: property + map: + title: label + options: options + explanation: description + translation: translation +destination: + plugin: entity:field_config + translations: true +migration_dependencies: + required: + - user_profile_field + - user_profile_field_instance diff --git a/core/modules/config_translation/src/Plugin/migrate/source/d6/I18nProfileField.php b/core/modules/config_translation/src/Plugin/migrate/source/d6/I18nProfileField.php new file mode 100644 index 0000000..ecdffd6 --- /dev/null +++ b/core/modules/config_translation/src/Plugin/migrate/source/d6/I18nProfileField.php @@ -0,0 +1,53 @@ +select('profile_fields', 'pf') + ->fields('pf', ['fid', 'name']) + ->fields('i18n', ['property']) + ->fields('lt', ['lid', 'translation', 'language']); + $query->leftJoin('i18n_strings', 'i18n', 'i18n.objectid = pf.name'); + $query->leftJoin('locales_target', 'lt', 'lt.lid = i18n.lid'); + return $query; + } + + /** + * {@inheritdoc} + */ + public function fields() { + return array( + 'fid' => $this->t('Profile field ID.'), + 'lid' => $this->t('Locales target language ID.'), + 'language' => $this->t('Language for this field.'), + 'translation' => $this->t('Translation of either the title or explanation.'), + ); + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['fid']['type'] = 'integer'; + $ids['language']['type'] = 'string'; + $ids['lid']['type'] = 'integer'; + $ids['lid']['alias'] = 'lt'; + return $ids; + } + +} diff --git a/core/modules/config_translation/tests/src/Kernel/Migrate/d6/MigrateI18nUserProfileFieldInstanceTest.php b/core/modules/config_translation/tests/src/Kernel/Migrate/d6/MigrateI18nUserProfileFieldInstanceTest.php new file mode 100644 index 0000000..057c14a --- /dev/null +++ b/core/modules/config_translation/tests/src/Kernel/Migrate/d6/MigrateI18nUserProfileFieldInstanceTest.php @@ -0,0 +1,64 @@ +executeMigrations([ + 'user_profile_field', + 'user_profile_field_instance', + 'd6_i18n_user_profile_field_instance', + ]); + $language_manager = $this->container->get('language_manager'); + + $config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_love_migrations'); + $this->assertSame("J'aime les migrations", $config_translation->get('label')); + $this->assertSame("Si vous cochez cette case, vous aimez les migrations.", $config_translation->get('description')); + + $config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_color'); + $this->assertSame('fr - Favorite color', $config_translation->get('label')); + $this->assertSame('Inscrivez votre couleur préférée', $config_translation->get('description')); + + $config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_biography'); + $this->assertSame('fr - Biography', $config_translation->get('label')); + $this->assertSame('fr - Tell people a little bit about yourself', $config_translation->get('description')); + + $config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_sell_address'); + $this->assertSame('fr - Sell your email address?', $config_translation->get('label')); + $this->assertSame("fr - If you check this box, we'll sell your address to spammers to help line the pockets of our shareholders. Thanks!", $config_translation->get('description')); + + $config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_sold_to'); + $this->assertSame('fr - Sales Category', $config_translation->get('label')); + $this->assertSame("fr - Select the sales categories to which this user's address was sold.", $config_translation->get('description')); + $this->assertSame('fr - Pill spammers Fitness spammers Back\slash Forward/slash Dot.in.the.middle', $config_translation->get('options')); + + $config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_bands'); + $this->assertSame('Mes groupes préférés', $config_translation->get('label')); + $this->assertSame("fr - Enter your favorite bands. When you've saved your profile, you'll be able to find other people with the same favorites.", $config_translation->get('description')); + + $config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_birthdate'); + $this->assertSame('fr - Birthdate', $config_translation->get('label')); + $this->assertSame('fr - Enter your birth date and we\'ll send you a coupon.', $config_translation->get('description')); + + $config_translation = $language_manager->getLanguageConfigOverride('fr', 'field.field.user.user.profile_blog'); + $this->assertSame('fr - Blog', $config_translation->get('label')); + $this->assertSame('fr - Paste the full URL, including http://, of your personal blog.', $config_translation->get('description')); + } + +} diff --git a/core/modules/config_translation/tests/src/Kernel/Plugin/migrate/source/d6/I18nProfileFieldTest.php b/core/modules/config_translation/tests/src/Kernel/Plugin/migrate/source/d6/I18nProfileFieldTest.php new file mode 100644 index 0000000..bd8d451 --- /dev/null +++ b/core/modules/config_translation/tests/src/Kernel/Plugin/migrate/source/d6/I18nProfileFieldTest.php @@ -0,0 +1,79 @@ + [ + [ + 'fid' => 42, + 'title' => 'I love migrations', + 'name' => 'profile_love_migrations', + ], + ], + 'i18n_strings' => [ + [ + 'lid' => 10, + 'objectid' => 'profile_love_migrations', + 'type' => 'field', + 'property' => 'title', + ], + [ + 'lid' => 11, + 'objectid' => 'profile_love_migrations', + 'type' => 'field', + 'property' => 'explanation' + ] + ], + 'locales_target' => [ + [ + 'lid' => 10, + 'translation' => "J'aime les migrations.", + 'language' => 'fr', + ], + [ + 'lid' => 11, + 'translation' => 'Si vous cochez cette case, vous aimez les migrations.', + 'language' => 'fr', + ], + ], + ]; + $test[0]['expected_results'] = [ + [ + 'property' => 'title', + 'translation' => "J'aime les migrations.", + 'language' => 'fr', + 'fid' => '42', + 'name' => 'profile_love_migrations', + ], + [ + 'property' => 'explanation', + 'translation' => 'Si vous cochez cette case, vous aimez les migrations.', + 'language' => 'fr', + 'fid' => '42', + 'name' => 'profile_love_migrations', + ], + ]; + return $test; + } + +} diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityConfigBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityConfigBase.php index 0cc00d6..a6f9259 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityConfigBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityConfigBase.php @@ -3,10 +3,16 @@ namespace Drupal\migrate\Plugin\migrate\destination; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\language\ConfigurableLanguageManager; +use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\MigrateException; use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\migrate\Row; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Class for importing configuration entities. @@ -21,6 +27,63 @@ class EntityConfigBase extends Entity { /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * The configuration factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface; + */ + protected $configFactory; + + /** + * Construct a new entity. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The storage for this entity type. + * @param array $bundles + * The list of bundles this entity type has. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The configuration factory. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles); + $this->languageManager = $language_manager; + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + $entity_type_id = static::getEntityTypeId($plugin_id); + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->get('entity.manager')->getStorage($entity_type_id), + array_keys($container->get('entity.manager')->getBundleInfo($entity_type_id)), + $container->get('language_manager'), + $container->get('config.factory') + ); + } + + /** * {@inheritdoc} */ public function import(Row $row, array $old_destination_id_values = array()) { @@ -39,17 +102,37 @@ public function import(Row $row, array $old_destination_id_values = array()) { } } $entity = $this->getEntity($row, $old_destination_id_values); - $entity->save(); + // Translations are already saved in updateEntity by configuration override. + if (!$this->isTranslationDestination()) { + $entity->save(); + } if (count($ids) > 1) { // This can only be a config entity, content entities have their ID key // and that's it. - $return = array(); + $return = []; foreach ($id_keys as $id_key) { - $return[] = $entity->get($id_key); + if (($this->isTranslationDestination()) && ($id_key == 'langcode')) { + // Config entities do not have a language property, get the language + // code from the destination. + $return[] = $row->getDestinationProperty($id_key); + } + else { + $return[] = $entity->get($id_key); + } } return $return; } - return array($entity->id()); + return [$entity->id()]; + } + + /** + * Get whether this destination is for translations. + * + * @return bool + * Whether this destination is for translations. + */ + protected function isTranslationDestination() { + return !empty($this->configuration['translations']); } /** @@ -58,6 +141,9 @@ public function import(Row $row, array $old_destination_id_values = array()) { public function getIds() { $id_key = $this->getKey('id'); $ids[$id_key]['type'] = 'string'; + if ($this->isTranslationDestination()) { + $ids['langcode']['type'] = 'string'; + } return $ids; } @@ -70,11 +156,28 @@ public function getIds() { * The row object to update from. */ protected function updateEntity(EntityInterface $entity, Row $row) { - foreach ($row->getRawDestination() as $property => $value) { - $this->updateEntityProperty($entity, explode(Row::PROPERTY_SEPARATOR, $property), $value); + // This is a translation if the language in the active config matches the + // language of this row. + $translation = FALSE; + if ($row->hasDestinationProperty('langcode') && $this->languageManager instanceof ConfigurableLanguageManager) { + $config = $entity->getConfigDependencyName(); + $langcode = $this->configFactory->get('langcode'); + if ($langcode != $row->getDestinationProperty('langcode')) { + $translation = TRUE; + } } - $this->setRollbackAction($row->getIdMap()); + if ($translation) { + $config_override = $this->languageManager->getLanguageConfigOverride($row->getDestinationProperty('langcode'), $config); + $config_override->set(str_replace(Row::PROPERTY_SEPARATOR, '.', $row->getDestinationProperty('property')), $row->getDestinationProperty('translation')); + $config_override->save(); + } + else { + foreach ($row->getRawDestination() as $property => $value) { + $this->updateEntityProperty($entity, explode(Row::PROPERTY_SEPARATOR, $property), $value); + } + $this->setRollbackAction($row->getIdMap()); + } } /** @@ -113,9 +216,39 @@ protected function updateEntityProperty(EntityInterface $entity, array $parents, protected function generateId(Row $row, array $ids) { $id_values = array(); foreach ($ids as $id) { + if ($this->isTranslationDestination() && $id == 'langcode') { + continue; + } $id_values[] = $row->getDestinationProperty($id); } return implode('.', $id_values); } + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifier) { + if ($this->isTranslationDestination()) { + // The entity id does not include the langcode. + $id_values = array(); + foreach ($destination_identifier as $key => $value) { + if ($this->isTranslationDestination() && $key == 'langcode') { + continue; + } + $id_values[] = $value; + } + $entity_id = implode('.', $id_values); + $language = $destination_identifier['langcode']; + + $config = $this->storage->load($entity_id)->getConfigDependencyName(); + $config_override = $this->languageManager->getLanguageConfigOverride($language, $config); + // Rollback the translation. + $config_override->delete(); + } + else { + $destination_identifier = implode('.', $destination_identifier); + parent::rollback(array($destination_identifier)); + } + } + } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldInstance.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldInstance.php index 966cb37..a431c49 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldInstance.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityFieldInstance.php @@ -18,15 +18,10 @@ public function getIds() { $ids['entity_type']['type'] = 'string'; $ids['bundle']['type'] = 'string'; $ids['field_name']['type'] = 'string'; + if ($this->isTranslationDestination()) { + $ids['langcode']['type'] = 'string'; + } return $ids; } - /** - * {@inheritdoc} - */ - public function rollback(array $destination_identifier) { - $destination_identifier = implode('.', $destination_identifier); - parent::rollback(array($destination_identifier)); - } - } diff --git a/core/modules/migrate/tests/src/Kernel/MigrateRollbackEntityConfigTest.php b/core/modules/migrate/tests/src/Kernel/MigrateRollbackEntityConfigTest.php new file mode 100644 index 0000000..b8e4811 --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/MigrateRollbackEntityConfigTest.php @@ -0,0 +1,171 @@ +installEntitySchema('taxonomy_vocabulary'); + $this->installEntitySchema('taxonomy_term'); + $this->installConfig(['taxonomy']); + } + + /** + * Tests rolling back configuration entity translations. + */ + public function testConfigEntityRollback() { + // We use vocabularies to demonstrate importing and rolling back + // configuration entities with translations. First, import vocabularies. + $vocabulary_data_rows = [ + ['id' => '1', 'name' => 'categories', 'weight' => '2'], + ['id' => '2', 'name' => 'tags', 'weight' => '1'], + ]; + $ids = ['id' => ['type' => 'integer']]; + $definition = [ + 'id' => 'vocabularies', + 'migration_tags' => ['Import and rollback test'], + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => $vocabulary_data_rows, + 'ids' => $ids, + ], + 'process' => [ + 'vid' => 'id', + 'name' => 'name', + 'weight' => 'weight', + ], + 'destination' => ['plugin' => 'entity:taxonomy_vocabulary'], + ]; + + /** @var \Drupal\migrate\Plugin\Migration $vocabulary_migration */ + $vocabulary_migration = \Drupal::service('plugin.manager.migration') + ->createStubMigration($definition); + $vocabulary_id_map = $vocabulary_migration->getIdMap(); + + $this->assertTrue($vocabulary_migration->getDestinationPlugin() + ->supportsRollback()); + + // Import and validate vocabulary config entities were created. + $vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this); + $vocabulary_executable->import(); + foreach ($vocabulary_data_rows as $row) { + /** @var Vocabulary $vocabulary */ + $vocabulary = Vocabulary::load($row['id']); + $this->assertTrue($vocabulary); + $map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]); + $this->assertNotNull($map_row['destid1']); + } + + // Second, import translations of the vocabulary name property. + $vocabulary_i18n_data_rows = [ + [ + 'id' => '1', + 'name' => '1', + 'language' => 'fr', + 'property' => 'name', + 'translation' => 'fr - categories' + ], + [ + 'id' => '2', + 'name' => '2', + 'language' => 'fr', + 'property' => 'name', + 'translation' => 'fr - tags' + ], + ]; + $ids = [ + 'id' => ['type' => 'integer'], + 'language' => ['type' => 'string'], + ]; + $definition = [ + 'id' => 'i18n_vocabularies', + 'migration_tags' => ['Import and rollback test'], + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => $vocabulary_i18n_data_rows, + 'ids' => $ids, + 'constants' => [ + 'name' => 'name', + ] + ], + 'process' => [ + 'vid' => 'id', + 'langcode' => 'language', + 'property' => 'constants/name', + 'translation' => 'translation', + ], + 'destination' => [ + 'plugin' => 'entity:taxonomy_vocabulary', + 'translations' => 'true', + ], + ]; + + $vocabulary_i18n__migration = \Drupal::service('plugin.manager.migration') + ->createStubMigration($definition); + $vocabulary_i18n_id_map = $vocabulary_i18n__migration->getIdMap(); + + $this->assertTrue($vocabulary_i18n__migration->getDestinationPlugin() + ->supportsRollback()); + + // Import and validate vocabulary config entities were created. + $vocabulary_i18n_executable = new MigrateExecutable($vocabulary_i18n__migration, $this); + $vocabulary_i18n_executable->import(); + + $language_manager = \Drupal::service('language_manager'); + foreach ($vocabulary_i18n_data_rows as $row) { + $langcode = $row['language']; + $id = 'taxonomy.vocabulary.' . $row['id']; + /** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */ + $config_translation = $language_manager->getLanguageConfigOverride($langcode, $id); + $this->assertSame($row['translation'], $config_translation->get('name')); + $map_row = $vocabulary_i18n_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]); + $this->assertNotNull($map_row['destid1']); + } + + // Perform the rollback and confirm the translation was deleted and the map + // table row removed. + $vocabulary_i18n_executable->rollback(); + foreach ($vocabulary_i18n_data_rows as $row) { + $langcode = $row['language']; + $id = 'taxonomy.vocabulary.' . $row['id']; + /** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */ + $config_translation = $language_manager->getLanguageConfigOverride($langcode, $id); + $this->assertNull($config_translation->get('name')); + $map_row = $vocabulary_i18n_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]); + $this->assertFalse($map_row); + } + + // Confirm the original vocabulary still exists. + foreach ($vocabulary_data_rows as $row) { + /** @var Vocabulary $vocabulary */ + $vocabulary = Vocabulary::load($row['id']); + $this->assertTrue($vocabulary); + $map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]); + $this->assertNotNull($map_row['destid1']); + } + + } + +} diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/destination/EntityFieldStorageConfig.php b/core/modules/migrate_drupal/src/Plugin/migrate/destination/EntityFieldStorageConfig.php index 169390a..a92a771 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/destination/EntityFieldStorageConfig.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/destination/EntityFieldStorageConfig.php @@ -2,8 +2,10 @@ namespace Drupal\migrate_drupal\Plugin\migrate\destination; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\migrate\destination\EntityFieldStorageConfig as BaseEntityFieldStorageConfig; @@ -45,11 +47,17 @@ class EntityFieldStorageConfig extends BaseEntityFieldStorageConfig { * The storage for this entity type. * @param array $bundles * The list of bundles this entity type has. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_plugin_manager * The field type plugin manager. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The configuration factory. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, FieldTypePluginManagerInterface $field_type_plugin_manager) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles); + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory,FieldTypePluginManagerInterface $field_type_plugin_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $language_manager, $config_factory, $field_type_plugin_manager); + $this->languageManager = $language_manager; + $this->configFactory = $config_factory; $this->fieldTypePluginManager = $field_type_plugin_manager; } @@ -65,6 +73,8 @@ public static function create(ContainerInterface $container, array $configuratio $migration, $container->get('entity.manager')->getStorage($entity_type_id), array_keys($container->get('entity.manager')->getBundleInfo($entity_type_id)), + $container->get('language_manager'), + $container->get('config.factory'), $container->get('plugin.manager.field.field_type') ); } diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php index 90b0b61..7d10e91 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -650,6 +650,10 @@ class MigrateUpgradeForm extends ConfirmFormBase { 'source_module' => 'profile', 'destination_module' => 'user', ], + 'd6_i18n_user_profile_field_instance' => [ + 'source_module' => 'i18n', + 'destination_module' => 'user', + ], ]; /**