Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -18,3 +18,9 @@ language.content_settings.*.*.third_party.content_translation:
enabled:
type: boolean
label: 'Content translation enabled'
bundle_settings:
type: sequence
label: 'Content translation bundle settings'
sequence:
type: string
label: 'Bundle settings values'

View file

@ -0,0 +1,140 @@
/**
* @file
* Content Translation admin behaviors.
*/
(function($, Drupal, drupalSettings) {
/**
* Forces applicable options to be checked as translatable.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches content translation dependent options to the UI.
*/
Drupal.behaviors.contentTranslationDependentOptions = {
attach(context) {
const $context = $(context);
const options = drupalSettings.contentTranslationDependentOptions;
let $fields;
function fieldsChangeHandler($fields, dependentColumns) {
return function(e) {
Drupal.behaviors.contentTranslationDependentOptions.check(
$fields,
dependentColumns,
$(e.target),
);
};
}
// We're given a generic name to look for so we find all inputs containing
// that name and copy over the input values that require all columns to be
// translatable.
if (options && options.dependent_selectors) {
Object.keys(options.dependent_selectors).forEach(field => {
$fields = $context.find(`input[name^="${field}"]`);
const dependentColumns = options.dependent_selectors[field];
$fields.on('change', fieldsChangeHandler($fields, dependentColumns));
Drupal.behaviors.contentTranslationDependentOptions.check(
$fields,
dependentColumns,
);
});
}
},
check($fields, dependentColumns, $changed) {
let $element = $changed;
let column;
function filterFieldsList(index, field) {
return $(field).val() === column;
}
// A field that has many different translatable parts can also define one
// or more columns that require all columns to be translatable.
Object.keys(dependentColumns || {}).forEach(index => {
column = dependentColumns[index];
if (!$changed) {
$element = $fields.filter(filterFieldsList);
}
if ($element.is(`input[value="${column}"]:checked`)) {
$fields
.prop('checked', true)
.not($element)
.prop('disabled', true);
} else {
$fields.prop('disabled', false);
}
});
},
};
/**
* Makes field translatability inherit bundle translatability.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches content translation behavior.
*/
Drupal.behaviors.contentTranslation = {
attach(context) {
// Initially hide all field rows for non translatable bundles and all
// column rows for non translatable fields.
$(context)
.find('table .bundle-settings .translatable :input')
.once('translation-entity-admin-hide')
.each(function() {
const $input = $(this);
const $bundleSettings = $input.closest('.bundle-settings');
if (!$input.is(':checked')) {
$bundleSettings.nextUntil('.bundle-settings').hide();
} else {
$bundleSettings
.nextUntil('.bundle-settings', '.field-settings')
.find('.translatable :input:not(:checked)')
.closest('.field-settings')
.nextUntil(':not(.column-settings)')
.hide();
}
});
// When a bundle is made translatable all of its fields should inherit
// this setting. Instead when it is made non translatable its fields are
// hidden, since their translatability no longer matters.
$('body')
.once('translation-entity-admin-bind')
.on('click', 'table .bundle-settings .translatable :input', e => {
const $target = $(e.target);
const $bundleSettings = $target.closest('.bundle-settings');
const $settings = $bundleSettings.nextUntil('.bundle-settings');
const $fieldSettings = $settings.filter('.field-settings');
if ($target.is(':checked')) {
$bundleSettings
.find('.operations :input[name$="[language_alterable]"]')
.prop('checked', true);
$fieldSettings.find('.translatable :input').prop('checked', true);
$settings.show();
} else {
$settings.hide();
}
})
.on('click', 'table .field-settings .translatable :input', e => {
const $target = $(e.target);
const $fieldSettings = $target.closest('.field-settings');
const $columnSettings = $fieldSettings.nextUntil(
'.field-settings, .bundle-settings',
);
if ($target.is(':checked')) {
$columnSettings.show();
} else {
$columnSettings.hide();
}
});
},
};
})(jQuery, Drupal, drupalSettings);

View file

@ -5,6 +5,8 @@
* The content translation administration forms.
*/
use Drupal\content_translation\BundleTranslationSettingsInterface;
use Drupal\content_translation\ContentTranslationManager;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
@ -63,7 +65,7 @@ function content_translation_field_sync_widget(FieldDefinitionInterface $field,
// does not get lost.
$element[key($options)]['#attached']['drupalSettings']['contentTranslationDependentOptions'] = [
'dependent_selectors' => [
$element_name => $require_all_groups_for_translation
$element_name => $require_all_groups_for_translation,
],
];
$element[key($options)]['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
@ -83,6 +85,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$
return;
}
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
$content_translation_manager = \Drupal::service('content_translation.manager');
$default = $form['entity_types']['#default_value'];
foreach ($default as $entity_type_id => $enabled) {
@ -110,6 +113,26 @@ function _content_translation_form_language_content_settings_form_alter(array &$
continue;
}
// Displayed the "shared fields widgets" toggle.
if ($content_translation_manager instanceof BundleTranslationSettingsInterface) {
$settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
$force_hidden = ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
$form['settings'][$entity_type_id][$bundle]['settings']['content_translation']['untranslatable_fields_hide'] = [
'#type' => 'checkbox',
'#title' => t('Hide non translatable fields on translation forms'),
'#default_value' => $force_hidden || !empty($settings['untranslatable_fields_hide']),
'#disabled' => $force_hidden,
'#description' => $force_hidden ? t('Moderated content requires non-translatable fields to be edited in the original language form.') : '',
'#states' => [
'visible' => [
':input[name="settings[' . $entity_type_id . '][' . $bundle . '][translatable]"]' => [
'checked' => TRUE,
],
],
],
];
}
$fields = $entity_manager->getFieldDefinitions($entity_type_id, $bundle);
if ($fields) {
foreach ($fields as $field_name => $definition) {
@ -141,6 +164,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$
$form['#validate'][] = 'content_translation_form_language_content_settings_validate';
$form['#submit'][] = 'content_translation_form_language_content_settings_submit';
}
/**
* Checks whether translatability should be configurable for a field.
*
@ -209,7 +233,7 @@ function _content_translation_preprocess_language_content_settings_table(&$varia
$rows[] = [
'data' => [
[
'data' => drupal_render($field_element),
'data' => \Drupal::service('renderer')->render($field_element),
'class' => ['translatable'],
],
[
@ -243,7 +267,7 @@ function _content_translation_preprocess_language_content_settings_table(&$varia
$rows[] = [
'data' => [
[
'data' => drupal_render($column_element[$key]),
'data' => \Drupal::service('renderer')->render($column_element[$key]),
'class' => ['translatable'],
],
[
@ -317,6 +341,8 @@ function content_translation_form_language_content_settings_validate(array $form
* @see content_translation_admin_settings_form_validate()
*/
function content_translation_form_language_content_settings_submit(array $form, FormStateInterface $form_state) {
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
$content_translation_manager = \Drupal::service('content_translation.manager');
$entity_types = $form_state->getValue('entity_types');
$settings = &$form_state->getValue('settings');
@ -347,7 +373,12 @@ function content_translation_form_language_content_settings_submit(array $form,
}
if (isset($bundle_settings['translatable'])) {
// Store whether a bundle has translation enabled or not.
\Drupal::service('content_translation.manager')->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
$content_translation_manager->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
// Store any other bundle settings.
if ($content_translation_manager instanceof BundleTranslationSettingsInterface) {
$content_translation_manager->setBundleTranslationSettings($entity_type_id, $bundle, $bundle_settings['settings']['content_translation']);
}
// Save translation_sync settings.
if (!empty($bundle_settings['columns'])) {
@ -367,8 +398,8 @@ function content_translation_form_language_content_settings_submit(array $form,
}
}
}
// Ensure entity and menu router information are correctly rebuilt.
\Drupal::entityManager()->clearCachedDefinitions();
\Drupal::service('router.builder')->setRebuildNeeded();
}

View file

@ -1,105 +1,69 @@
/**
* @file
* Content Translation admin behaviors.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal, drupalSettings) {
'use strict';
/**
* Forces applicable options to be checked as translatable.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches content translation dependent options to the UI.
*/
Drupal.behaviors.contentTranslationDependentOptions = {
attach: function (context) {
attach: function attach(context) {
var $context = $(context);
var options = drupalSettings.contentTranslationDependentOptions;
var $fields;
var dependent_columns;
var $fields = void 0;
function fieldsChangeHandler($fields, dependent_columns) {
function fieldsChangeHandler($fields, dependentColumns) {
return function (e) {
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependent_columns, $(e.target));
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns, $(e.target));
};
}
// We're given a generic name to look for so we find all inputs containing
// that name and copy over the input values that require all columns to be
// translatable.
if (options && options.dependent_selectors) {
for (var field in options.dependent_selectors) {
if (options.dependent_selectors.hasOwnProperty(field)) {
$fields = $context.find('input[name^="' + field + '"]');
dependent_columns = options.dependent_selectors[field];
Object.keys(options.dependent_selectors).forEach(function (field) {
$fields = $context.find('input[name^="' + field + '"]');
var dependentColumns = options.dependent_selectors[field];
$fields.on('change', fieldsChangeHandler($fields, dependent_columns));
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependent_columns);
}
}
$fields.on('change', fieldsChangeHandler($fields, dependentColumns));
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns);
});
}
},
check: function ($fields, dependent_columns, $changed) {
check: function check($fields, dependentColumns, $changed) {
var $element = $changed;
var column;
var column = void 0;
function filterFieldsList(index, field) {
return $(field).val() === column;
}
// A field that has many different translatable parts can also define one
// or more columns that require all columns to be translatable.
for (var index in dependent_columns) {
if (dependent_columns.hasOwnProperty(index)) {
column = dependent_columns[index];
if (!$changed) {
$element = $fields.filter(filterFieldsList);
}
if ($element.is('input[value="' + column + '"]:checked')) {
$fields.prop('checked', true)
.not($element).prop('disabled', true);
}
else {
$fields.prop('disabled', false);
}
Object.keys(dependentColumns || {}).forEach(function (index) {
column = dependentColumns[index];
if (!$changed) {
$element = $fields.filter(filterFieldsList);
}
}
if ($element.is('input[value="' + column + '"]:checked')) {
$fields.prop('checked', true).not($element).prop('disabled', true);
} else {
$fields.prop('disabled', false);
}
});
}
};
/**
* Makes field translatability inherit bundle translatability.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches content translation behavior.
*/
Drupal.behaviors.contentTranslation = {
attach: function (context) {
// Initially hide all field rows for non translatable bundles and all
// column rows for non translatable fields.
attach: function attach(context) {
$(context).find('table .bundle-settings .translatable :input').once('translation-entity-admin-hide').each(function () {
var $input = $(this);
var $bundleSettings = $input.closest('.bundle-settings');
if (!$input.is(':checked')) {
$bundleSettings.nextUntil('.bundle-settings').hide();
}
else {
} else {
$bundleSettings.nextUntil('.bundle-settings', '.field-settings').find('.translatable :input:not(:checked)').closest('.field-settings').nextUntil(':not(.column-settings)').hide();
}
});
// When a bundle is made translatable all of its fields should inherit
// this setting. Instead when it is made non translatable its fields are
// hidden, since their translatability no longer matters.
$('body').once('translation-entity-admin-bind').on('click', 'table .bundle-settings .translatable :input', function (e) {
var $target = $(e.target);
var $bundleSettings = $target.closest('.bundle-settings');
@ -109,23 +73,19 @@
$bundleSettings.find('.operations :input[name$="[language_alterable]"]').prop('checked', true);
$fieldSettings.find('.translatable :input').prop('checked', true);
$settings.show();
}
else {
} else {
$settings.hide();
}
})
.on('click', 'table .field-settings .translatable :input', function (e) {
var $target = $(e.target);
var $fieldSettings = $target.closest('.field-settings');
var $columnSettings = $fieldSettings.nextUntil('.field-settings, .bundle-settings');
if ($target.is(':checked')) {
$columnSettings.show();
}
else {
$columnSettings.hide();
}
});
}).on('click', 'table .field-settings .translatable :input', function (e) {
var $target = $(e.target);
var $fieldSettings = $target.closest('.field-settings');
var $columnSettings = $fieldSettings.nextUntil('.field-settings, .bundle-settings');
if ($target.is(':checked')) {
$columnSettings.show();
} else {
$columnSettings.hide();
}
});
}
};
})(jQuery, Drupal, drupalSettings);
})(jQuery, Drupal, drupalSettings);

View file

@ -2,7 +2,7 @@ name: 'Content Translation'
type: module
description: 'Allows users to translate content entities.'
dependencies:
- language
- drupal:language
package: Multilingual
version: VERSION
core: 8.x

View file

@ -5,7 +5,9 @@
* Installation functions for Content Translation module.
*/
use \Drupal\Core\Url;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
/**
* Implements hook_install().
@ -18,17 +20,17 @@ function content_translation_install() {
// Translation works when at least two languages are added.
if (count(\Drupal::languageManager()->getLanguages()) < 2) {
$t_args = [
':language_url' => Url::fromRoute('entity.configurable_language.collection')->toString()
':language_url' => Url::fromRoute('entity.configurable_language.collection')->toString(),
];
$message = t('This site has only a single language enabled. <a href=":language_url">Add at least one more language</a> in order to translate content.', $t_args);
drupal_set_message($message, 'warning');
\Drupal::messenger()->addWarning($message);
}
// Point the user to the content translation settings.
$t_args = [
':settings_url' => Url::fromRoute('language.content_settings_page')->toString()
':settings_url' => Url::fromRoute('language.content_settings_page')->toString(),
];
$message = t('<a href=":settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, or any other element you wish to translate.', $t_args);
drupal_set_message($message, 'warning');
\Drupal::messenger()->addWarning($message);
}
/**
@ -44,3 +46,58 @@ function content_translation_update_8001() {
function content_translation_update_8002() {
\Drupal::service('plugin.manager.field.field_type')->clearCachedDefinitions();
}
/**
* Fix the initial values for content translation metadata fields.
*/
function content_translation_update_8400() {
$database = \Drupal::database();
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
$content_translation_manager = \Drupal::service('content_translation.manager');
/** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */
$last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
$entity_type_manager = \Drupal::entityTypeManager();
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$entity_type_manager->clearCachedDefinitions();
foreach ($content_translation_manager->getSupportedEntityTypes() as $entity_type_id => $entity_type_definition) {
$storage = $entity_type_manager->getStorage($entity_type_id);
if ($storage instanceof SqlEntityStorageInterface) {
$entity_type = $entity_definition_update_manager->getEntityType($entity_type_id);
$storage_definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id);
// Since the entity type is managed by Content Translation, we can assume
// that it is translatable, so we use the data and revision data tables.
$tables_to_update = [$entity_type->getDataTable()];
if ($entity_type->isRevisionable()) {
$tables_to_update += [$entity_type->getRevisionDataTable()];
}
foreach ($tables_to_update as $table_name) {
// Fix the values of the 'content_translation_source' field.
if (isset($storage_definitions['content_translation_source'])) {
$database->update($table_name)
->fields(['content_translation_source' => LanguageInterface::LANGCODE_NOT_SPECIFIED])
->isNull('content_translation_source')
->execute();
}
// Fix the values of the 'content_translation_outdated' field.
if (isset($storage_definitions['content_translation_outdated'])) {
$database->update($table_name)
->fields(['content_translation_outdated' => 0])
->isNull('content_translation_outdated')
->execute();
}
// Fix the values of the 'content_translation_status' field.
if (isset($storage_definitions['content_translation_status'])) {
$database->update($table_name)
->fields(['content_translation_status' => 1])
->isNull('content_translation_status')
->execute();
}
}
}
}
}

View file

@ -5,6 +5,8 @@
* Allows entities to be translated into different languages.
*/
use Drupal\content_translation\BundleTranslationSettingsInterface;
use Drupal\content_translation\ContentTranslationManager;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Entity\ContentEntityInterface;
@ -59,6 +61,14 @@ function content_translation_module_implements_alter(&$implementations, $hook) {
unset($implementations['content_translation']);
$implementations['content_translation'] = $group;
break;
// Move our hook_entity_bundle_info_alter() implementation to the top of the
// list, so that any other hook implementation can rely on bundles being
// correctly marked as translatable.
case 'entity_bundle_info_alter':
$group = $implementations['content_translation'];
$implementations = ['content_translation' => $group] + $implementations;
break;
}
}
@ -154,6 +164,8 @@ function content_translation_entity_type_alter(array &$entity_types) {
}
$entity_type->set('translation', $translation);
}
$entity_type->addConstraint('ContentTranslationSynchronizedFields');
}
}
@ -161,9 +173,19 @@ function content_translation_entity_type_alter(array &$entity_types) {
* Implements hook_entity_bundle_info_alter().
*/
function content_translation_entity_bundle_info_alter(&$bundles) {
foreach ($bundles as $entity_type => &$info) {
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
$content_translation_manager = \Drupal::service('content_translation.manager');
foreach ($bundles as $entity_type_id => &$info) {
foreach ($info as $bundle => &$bundle_info) {
$bundle_info['translatable'] = \Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle);
$bundle_info['translatable'] = $content_translation_manager->isEnabled($entity_type_id, $bundle);
if ($bundle_info['translatable'] && $content_translation_manager instanceof BundleTranslationSettingsInterface) {
$settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
// If pending revision support is enabled for this bundle, we need to
// hide untranslatable field widgets, otherwise changes in pending
// revisions might be overridden by changes in later default revisions.
$bundle_info['untranslatable_fields.default_translation_affected'] =
!empty($settings['untranslatable_fields_hide']) || ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
}
}
}
}
@ -183,8 +205,8 @@ function content_translation_entity_base_field_info(EntityTypeInterface $entity_
// when translation is disabled.
// @todo Re-evaluate this approach and consider removing field storage
// definitions and the related field data if the entity type has no bundle
// enabled for translation, once base field purging is supported.
// See https://www.drupal.org/node/2282119.
// enabled for translation.
// @see https://www.drupal.org/node/2907777
if ($manager->isEnabled($entity_type_id) || array_intersect_key($definitions, $installed_storage_definitions)) {
return $definitions;
}
@ -311,14 +333,21 @@ function content_translation_form_alter(array &$form, FormStateInterface $form_s
// Handle fields shared between translations when there is at least one
// translation available or a new one is being created.
if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) {
$langcode_key = $entity->getEntityType()->getKey('langcode');
foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
if (isset($form[$field_name]) && $field_name != $langcode_key) {
// Allow the widget to define if it should be treated as multilingual
// by respecting an already set #multilingual key.
if (isset($form[$field_name]) && !isset($form[$field_name]['#multilingual'])) {
$form[$field_name]['#multilingual'] = $definition->isTranslatable();
}
}
}
// The footer region, if defined, may contain multilingual widgets so we
// need to always display it.
if (isset($form['footer'])) {
$form['footer']['#multilingual'] = TRUE;
}
}
}
@ -410,6 +439,11 @@ function content_translation_form_field_config_edit_form_alter(array &$form, For
*/
function content_translation_entity_presave(EntityInterface $entity) {
if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && !$entity->isNew()) {
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
$manager = \Drupal::service('content_translation.manager');
if (!$manager->isEnabled($entity->getEntityTypeId(), $entity->bundle())) {
return;
}
// If we are creating a new translation we need to use the source language
// as original language, since source values are the only ones available to
// compare against.
@ -418,8 +452,6 @@ function content_translation_entity_presave(EntityInterface $entity) {
->getStorage($entity->entityType())->loadUnchanged($entity->id());
}
$langcode = $entity->language()->getId();
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
$manager = \Drupal::service('content_translation.manager');
$source_langcode = !$entity->original->hasTranslation($langcode) ? $manager->getTranslationMetadata($entity)->getSource() : NULL;
\Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $langcode, $source_langcode);
}

View file

@ -1,7 +1,7 @@
services:
content_translation.synchronizer:
class: Drupal\content_translation\FieldTranslationSynchronizer
arguments: ['@entity.manager']
arguments: ['@entity.manager', '@plugin.manager.field.field_type']
content_translation.subscriber:
class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber
@ -9,6 +9,12 @@ services:
tags:
- { name: event_subscriber }
content_translation.delete_access:
class: Drupal\content_translation\Access\ContentTranslationDeleteAccess
arguments: ['@entity_type.manager', '@content_translation.manager']
tags:
- { name: access_check, applies_to: _access_content_translation_delete }
content_translation.overview_access:
class: Drupal\content_translation\Access\ContentTranslationOverviewAccess
arguments: ['@entity.manager']

View file

@ -0,0 +1,70 @@
id: d6_block_translation
label: Block translations
migration_tags:
- Drupal 6
- Configuration
- Multilingual
source:
plugin: d6_block_translation
constants:
dest_label: 'settings/label'
process:
langcode: language
property: constants/dest_label
translation: title
id:
-
plugin: migration_lookup
migration: d6_block
source:
- module
- delta
-
plugin: skip_on_empty
method: row
plugin:
-
plugin: static_map
bypass: true
source:
- module
- delta
map:
book:
0: book_navigation
comment:
0: views_block:comments_recent-block_1
forum:
0: forum_active_block
1: forum_new_block
locale:
0: language_block
node:
0: node_syndicate_block
search:
0: search_form_block
statistics:
0: statistics_popular_block
system:
0: system_powered_by_block
user:
0: user_login_block
1: system_menu_block:tools
2: views_block:who_s_new-block_1
3: views_block:who_s_online-who_s_online_block
-
plugin: block_plugin_id
-
plugin: skip_on_empty
method: row
theme:
plugin: block_theme
source:
- theme
- default_theme
- admin_theme
destination:
plugin: entity:block
migration_dependencies:
required:
- d6_block

View file

@ -0,0 +1,50 @@
id: d6_custom_block_translation
label: Custom block translations
migration_tags:
- Drupal 6
- Content
- Multilingual
source:
plugin: d6_box_translation
process:
id:
plugin: migration_lookup
migration: d6_custom_block
source:
- bid
langcode: language
info:
-
plugin: callback
source:
- title_translated
- title
callable: array_filter
-
plugin: callback
callable: current
'body/value':
-
plugin: callback
source:
- body_translated
- body
callable: array_filter
-
plugin: callback
callable: current
'body/format':
plugin: migration_lookup
migration: d6_filter_format
source: format
destination:
plugin: entity:block_content
no_stub: true
translations: true
destination_module: content_translation
migration_dependencies:
required:
- d6_filter_format
- block_content_body_field
- d6_custom_block
- language

View file

@ -0,0 +1,26 @@
id: d6_entity_reference_translation
label: Entity reference translations
migration_tags:
- Drupal 6
- Multilingual
- Follow-up migration
deriver: Drupal\migrate_drupal\Plugin\migrate\EntityReferenceTranslationDeriver
provider:
- content_translation
- migrate_drupal
# Supported target types for entity reference translation migrations. The array
# keys are the supported target types and the values are arrays of migrations
# to lookup for the translated entity IDs.
target_types:
node:
- d6_node_translation
# The source plugin will be set by the deriver.
source:
plugin: empty
key: default
target: default
# The process pipeline will be set by the deriver.
process: []
# The destination plugin will be set by the deriver.
destination:
plugin: null

View file

@ -0,0 +1,55 @@
id: d6_menu_links_translation
label: Menu links
migration_tags:
- Drupal 6
- Content
- Multilingual
source:
plugin: d6_menu_link_translation
process:
id: mlid
langcode: language
title:
-
plugin: callback
source:
- title_translated
- link_title
callable: array_filter
-
plugin: callback
callable: current
description:
-
plugin: callback
source:
- description_translated
- description
callable: array_filter
-
plugin: callback
callable: current
menu_name:
-
plugin: migration_lookup
# The menu migration is in the system module.
migration: d6_menu
source: menu_name
-
plugin: skip_on_empty
method: row
-
plugin: static_map
map:
management: admin
bypass: true
destination:
plugin: entity:menu_link_content
default_bundle: menu_link_content
no_stub: true
translations: true
migration_dependencies:
required:
- language
- d6_menu
- d6_menu_links

View file

@ -0,0 +1,59 @@
id: d6_node_translation
label: Node translations
migration_tags:
- Drupal 6
- translation
- Content
- Multilingual
class: Drupal\node\Plugin\migrate\D6NodeTranslation
deriver: Drupal\node\Plugin\migrate\D6NodeDeriver
source:
plugin: d6_node
translations: true
process:
# If you are using this file to build a custom migration consider removing
# the nid field to allow incremental migrations.
nid: tnid
type: type
langcode:
plugin: default_value
source: language
default_value: "und"
title: title
uid: node_uid
status: status
created: created
changed: changed
promote: promote
sticky: sticky
'body/format':
plugin: migration_lookup
migration: d6_filter_format
source: format
'body/value': body
'body/summary': teaser
revision_uid: revision_uid
revision_log: log
revision_timestamp: timestamp
content_translation_source: source_langcode
# unmapped d6 fields.
# translate
# moderate
# comment
destination:
plugin: entity:node
translations: true
destination_module: content_translation
migration_dependencies:
required:
- d6_user
- d6_node_type
- d6_node_settings
- d6_filter_format
- language
optional:
- d6_field_instance_widget_settings
- d6_field_formatter_settings
- d6_upload_field_instance

View file

@ -0,0 +1,42 @@
id: d6_taxonomy_term_translation
label: Taxonomy terms
migration_tags:
- Drupal 6
- Content
- Multilingual
source:
plugin: d6_taxonomy_term
translations: true
process:
# If you are using this file to build a custom migration consider removing
# the tid field to allow incremental migrations.
tid: tid
langcode: language
vid:
plugin: migration
migration: d6_taxonomy_vocabulary
source: vid
name: name
description: description
weight: weight
# Only attempt to stub real (non-zero) parents.
parent_id:
-
plugin: skip_on_empty
method: process
source: parent
-
plugin: migration
migration: d6_taxonomy_term
parent:
plugin: default_value
default_value: 0
source: '@parent_id'
changed: timestamp
destination:
plugin: entity:taxonomy_term
destination_module: content_translation
migration_dependencies:
required:
- d6_taxonomy_vocabulary
- d6_taxonomy_term

View file

@ -0,0 +1,28 @@
id: d7_comment_entity_translation
label: Comment entity translations
migration_tags:
- Drupal 7
- translation
- Content
class: Drupal\comment\Plugin\migrate\D7Comment
source:
plugin: d7_comment_entity_translation
process:
cid: entity_id
subject: subject
langcode: language
uid: uid
status: status
created: created
changed: changed
content_translation_source: source
content_translation_outdated: translate
destination:
plugin: entity:comment
translations: true
destination_module: content_translation
migration_dependencies:
required:
- language
- d7_entity_translation_settings
- d7_comment

View file

@ -0,0 +1,50 @@
id: d7_custom_block_translation
label: Custom block translations
migration_tags:
- Drupal 7
- Content
- Multilingual
source:
plugin: d7_block_custom_translation
process:
id:
plugin: migration_lookup
migration: d7_custom_block
source:
- bid
langcode: language
info:
-
plugin: callback
source:
- title_translated
- title
callable: array_filter
-
plugin: callback
callable: current
'body/value':
-
plugin: callback
source:
- body_translated
- body
callable: array_filter
-
plugin: callback
callable: current
'body/format':
plugin: migration_lookup
migration: d7_filter_format
source: format
destination:
plugin: entity:block_content
no_stub: true
translations: true
destination_module: content_translation
migration_dependencies:
required:
- d7_filter_format
- block_content_body_field
- d7_custom_block
- language

View file

@ -0,0 +1,26 @@
id: d7_entity_reference_translation
label: Entity reference translations
migration_tags:
- Drupal 7
- Multilingual
- Follow-up migration
deriver: Drupal\migrate_drupal\Plugin\migrate\EntityReferenceTranslationDeriver
provider:
- content_translation
- migrate_drupal
# Supported target types for entity reference translation migrations. The array
# keys are the supported target types and the values are arrays of migrations
# to lookup for the translated entity IDs.
target_types:
node:
- d7_node_translation
# The source plugin will be set by the deriver.
source:
plugin: empty
key: default
target: default
# The process pipeline will be set by the deriver.
process: []
# The destination plugin will be set by the deriver.
destination:
plugin: null

View file

@ -0,0 +1,37 @@
id: d7_entity_translation_settings
label: Drupal 7 Entity Translation settings
migration_tags:
- Drupal 7
- Configuration
- Multilingual
source:
plugin: d7_entity_translation_settings
process:
id: id
target_entity_type_id: target_entity_type_id
target_bundle: target_bundle
default_langcode:
plugin: static_map
source: default_langcode
bypass: true
map:
xx-et-default: site_default
xx-et-current: current_interface
xx-et-author: authors_default
language_alterable: language_alterable
third_party_settings/content_translation/enabled:
plugin: default_value
default_value: true
third_party_settings/content_translation/bundle_settings/untranslatable_fields_hide: untranslatable_fields_hide
destination:
plugin: entity:language_content_settings
content_translation_update_definitions:
- comment
- node
- taxonomy_term
- user
migration_dependencies:
optional:
- d7_comment_type
- d7_node_type
- d7_taxonomy_vocabulary

View file

@ -0,0 +1,36 @@
id: d7_node_entity_translation
label: Node entity translations
migration_tags:
- Drupal 7
- translation
- Content
- Multilingual
deriver: Drupal\node\Plugin\migrate\D7NodeDeriver
source:
plugin: d7_node_entity_translation
process:
nid: entity_id
type: type
langcode: language
title: title
uid: uid
status: status
created: created
changed: changed
promote: promote
sticky: sticky
revision_uid: revision_uid
revision_log: log
revision_timestamp: timestamp
content_translation_source: source
# Boolean indicating whether this translation needs to be updated.
content_translation_outdated: translate
destination:
plugin: entity:node
translations: true
destination_module: content_translation
migration_dependencies:
required:
- language
- d7_entity_translation_settings
- d7_node

View file

@ -0,0 +1,45 @@
id: d7_node_translation
label: Node translations
migration_tags:
- Drupal 7
- translation
- Content
- Multilingual
class: Drupal\node\Plugin\migrate\D7NodeTranslation
deriver: Drupal\node\Plugin\migrate\D7NodeDeriver
source:
plugin: d7_node
translations: true
process:
# If you are using this file to build a custom migration consider removing
# the nid field to allow incremental migrations.
nid: tnid
type: type
langcode:
plugin: default_value
source: language
default_value: "und"
title: title
uid: node_uid
status: status
created: created
changed: changed
promote: promote
sticky: sticky
revision_uid: revision_uid
revision_log: log
revision_timestamp: timestamp
content_translation_source: source_langcode
destination:
plugin: entity:node
translations: true
content_translation_update_definitions:
- node
destination_module: content_translation
migration_dependencies:
required:
- d7_user
- d7_node_type
- language
optional:
- d7_field_instance

View file

@ -0,0 +1,32 @@
id: d7_taxonomy_term_entity_translation
label: Taxonomy term entity translations
migration_tags:
- Drupal 7
- translation
- Content
- Multilingual
deriver: Drupal\taxonomy\Plugin\migrate\D7TaxonomyTermDeriver
source:
plugin: d7_taxonomy_term_entity_translation
process:
tid: entity_id
name: name
description/value: description
description/format: format
langcode: language
status: status
content_translation_source: source
content_translation_outdated: translate
content_translation_uid: uid
content_translation_created: created
changed: changed
forum_container: is_container
destination:
plugin: entity:taxonomy_term
translations: true
destination_module: content_translation
migration_dependencies:
required:
- language
- d7_entity_translation_settings
- d7_taxonomy_term

View file

@ -0,0 +1,27 @@
id: d7_user_entity_translation
label: User accounts entity translations
migration_tags:
- Drupal 7
- translation
- Content
- Multilingual
class: Drupal\user\Plugin\migrate\User
source:
plugin: d7_user_entity_translation
process:
uid: entity_id
langcode: language
content_translation_source: source
content_translation_uid: uid
content_translation_status: status
content_translation_outdated: translate
content_translation_created: created
destination:
plugin: entity:user
translations: true
destination_module: content_translation
migration_dependencies:
required:
- language
- d7_entity_translation_settings
- d7_user

View file

@ -0,0 +1,120 @@
id: node_translation_menu_links
label: Node Translations Menu links
audit: true
migration_tags:
- Drupal 6
- Drupal 7
- Content
- Multilingual
source:
plugin: menu_link
constants:
entity_prefix: 'entity:'
node_prefix: 'node/'
process:
id: mlid
title: link_title
description: description
menu_name:
-
plugin: migration_lookup
# The menu migration is in the system module.
migration:
- d6_menu
- d7_menu
source: menu_name
-
plugin: skip_on_empty
method: row
-
plugin: static_map
map:
management: admin
bypass: true
# In this process pipeline, given a menu link path that might be for a
# translated node which has been merged with the default language node, we are
# trying to determine the new node ID, that is the ID of the default language
# node.
new_nid:
-
# If the path is of the form "node/<ID>" and is not routed, we will get
# back an URI of the form "base:node/<ID>".
plugin: link_uri
source:
- link_path
validate_route: false
-
# Isolate the node ID.
plugin: explode
delimiter: 'base:node/'
-
# Extract the node ID.
plugin: extract
default: false
index:
- 1
-
# Skip row if node ID is empty.
plugin: skip_on_empty
method: row
-
# With the old node ID in hand, lookup in the d6_node_translation or
# d7_node_translation mapping tables to find the new node ID.
plugin: migration_lookup
migration:
- d6_node_translation
- d7_node_translation
no_stub: true
-
# Skip row if the new node ID is empty.
plugin: skip_on_empty
method: row
-
# Extract the node ID. The migration lookup will return an array with two
# items, the new node ID and the translation langcode. We need the node ID
# which is at index 0.
plugin: extract
index:
- 0
# This will be used in the "link/uri" and "route" processes below.
link_path:
plugin: concat
source:
- 'constants/node_prefix'
- '@new_nid'
link/uri:
plugin: concat
source:
- 'constants/entity_prefix'
- '@link_path'
link/options: options
route:
plugin: route
source:
- '@link_path'
- options
route_name: '@route/route_name'
route_parameters: '@route/route_parameters'
url: '@route/url'
options: '@route/options'
external: external
weight: weight
expanded: expanded
enabled: enabled
parent:
plugin: menu_link_parent
source:
- plid
- '@menu_name'
- parent_link_path
changed: updated
destination:
plugin: entity:menu_link_content
default_bundle: menu_link_content
no_stub: true
migration_dependencies:
optional:
- d6_menu_links
- d6_node_translation
- d7_menu_links
- d7_node_translation

View file

@ -0,0 +1,35 @@
id: statistics_node_translation_counter
label: Node translation counter
migration_tags:
- Drupal 6
- Drupal 7
- Content
- Multilingual
source:
plugin: node_counter
process:
nid:
-
plugin: migration_lookup
migration:
- d6_node_translation
- d7_node_translation
source: nid
-
plugin: skip_on_empty
method: row
-
plugin: extract
index:
- 0
totalcount: totalcount
daycount: daycount
timestamp: timestamp
destination:
plugin: node_counter
migration_dependencies:
required:
- statistics_node_counter
optional:
- d6_node_translation
- d7_node_translation

View file

@ -0,0 +1,124 @@
<?php
namespace Drupal\content_translation\Access;
use Drupal\content_translation\ContentTranslationManager;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\language\Entity\ContentLanguageSettings;
use Drupal\workflows\Entity\Workflow;
/**
* Access check for entity translation deletion.
*
* @internal This additional access checker only aims to prevent deletions in
* pending revisions until we are able to flag revision translations as
* deleted.
*
* @todo Remove this in https://www.drupal.org/node/2945956.
*/
class ContentTranslationDeleteAccess implements AccessInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* Constructs a ContentTranslationDeleteAccess object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $manager
* The entity type manager.
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* The content translation manager.
*/
public function __construct(EntityTypeManagerInterface $manager, ContentTranslationManagerInterface $content_translation_manager) {
$this->entityTypeManager = $manager;
$this->contentTranslationManager = $content_translation_manager;
}
/**
* Checks access to translation deletion for the specified route match.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parameterized route.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
$requirement = $route_match->getRouteObject()->getRequirement('_access_content_translation_delete');
$entity_type_id = current(explode('.', $requirement));
$entity = $route_match->getParameter($entity_type_id);
return $this->checkAccess($entity);
}
/**
* Checks access to translation deletion for the specified entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity translation to be deleted.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function checkAccess(ContentEntityInterface $entity) {
$result = AccessResult::allowed();
$entity_type_id = $entity->getEntityTypeId();
$result->addCacheableDependency($entity);
// Add the cache dependencies used by
// ContentTranslationManager::isPendingRevisionSupportEnabled().
if (\Drupal::moduleHandler()->moduleExists('content_moderation')) {
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
$result->addCacheableDependency($workflow);
}
}
if (!ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) {
return $result;
}
if ($entity->isDefaultTranslation()) {
return $result;
}
$config = ContentLanguageSettings::load($entity_type_id . '.' . $entity->bundle());
$result->addCacheableDependency($config);
if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
return $result;
}
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $entity->language()->getId());
if (!$revision_id) {
return $result;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision = $storage->loadRevision($revision_id);
if ($revision->wasDefaultRevision()) {
return $result;
}
$result = $result->andIf(AccessResult::forbidden());
return $result;
}
}

View file

@ -3,6 +3,7 @@
namespace Drupal\content_translation\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
@ -90,15 +91,12 @@ class ContentTranslationManageAccessCheck implements AccessInterface {
return AccessResult::allowed()->cachePerPermissions();
}
/* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
$handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation');
// Load translation.
$translations = $entity->getTranslationLanguages();
$languages = $this->languageManager->getLanguages();
switch ($operation) {
case 'create':
/* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
$handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation');
$translations = $entity->getTranslationLanguages();
$languages = $this->languageManager->getLanguages();
$source_language = $this->languageManager->getLanguage($source) ?: $entity->language();
$target_language = $this->languageManager->getLanguage($target) ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
$is_new_translation = ($source_language->getId() != $target_language->getId()
@ -109,12 +107,14 @@ class ContentTranslationManageAccessCheck implements AccessInterface {
->andIf($handler->getTranslationAccess($entity, $operation));
case 'delete':
// @todo Remove this in https://www.drupal.org/node/2945956.
/** @var \Drupal\Core\Access\AccessResultInterface $delete_access */
$delete_access = \Drupal::service('content_translation.delete_access')->checkAccess($entity);
$access = $this->checkAccess($entity, $language, $operation);
return $delete_access->andIf($access);
case 'update':
$has_translation = isset($languages[$language->getId()])
&& $language->getId() != $entity->getUntranslated()->language()->getId()
&& isset($translations[$language->getId()]);
return AccessResult::allowedIf($has_translation)->cachePerPermissions()->addCacheableDependency($entity)
->andIf($handler->getTranslationAccess($entity, $operation));
return $this->checkAccess($entity, $language, $operation);
}
}
@ -122,4 +122,30 @@ class ContentTranslationManageAccessCheck implements AccessInterface {
return AccessResult::neutral();
}
/**
* Performs access checks for the specified operation.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being checked.
* @param \Drupal\Core\Language\LanguageInterface $language
* For an update or delete operation, the language code of the translation
* being updated or deleted.
* @param string $operation
* The operation to be checked.
*
* @return \Drupal\Core\Access\AccessResultInterface
* An access result object.
*/
protected function checkAccess(ContentEntityInterface $entity, LanguageInterface $language, $operation) {
/* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
$handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation');
$translations = $entity->getTranslationLanguages();
$languages = $this->languageManager->getLanguages();
$has_translation = isset($languages[$language->getId()])
&& $language->getId() != $entity->getUntranslated()->language()->getId()
&& isset($translations[$language->getId()]);
return AccessResult::allowedIf($has_translation)->cachePerPermissions()->addCacheableDependency($entity)
->andIf($handler->getTranslationAccess($entity, $operation));
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\content_translation;
/**
* Interface providing support for content translation bundle settings.
*/
interface BundleTranslationSettingsInterface {
/**
* Returns translation settings for the specified bundle.
*
* @param string $entity_type_id
* The entity type identifier.
* @param string $bundle
* The bundle name.
*
* @return array
* An associative array of values keyed by setting name.
*/
public function getBundleTranslationSettings($entity_type_id, $bundle);
/**
* Sets translation settings for the specified bundle.
*
* @param string $entity_type_id
* The entity type identifier.
* @param string $bundle
* The bundle name.
* @param array $settings
* An associative array of values keyed by setting name.
*/
public function setBundleTranslationSettings($entity_type_id, $bundle, array $settings);
}

View file

@ -5,6 +5,7 @@ namespace Drupal\content_translation;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityChangesDetectionTrait;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
@ -13,8 +14,10 @@ use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\user\Entity\User;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -25,7 +28,10 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @ingroup entity_api
*/
class ContentTranslationHandler implements ContentTranslationHandlerInterface, EntityHandlerInterface {
use EntityChangesDetectionTrait;
use DependencySerializationTrait;
use StringTranslationTrait;
/**
* The type of the entity being translated.
@ -55,6 +61,13 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
*/
protected $manager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current user.
*
@ -70,6 +83,13 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
*/
protected $fieldStorageDefinitions;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Initializes an instance of the content translation controller.
*
@ -83,14 +103,18 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
* The entity manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(EntityTypeInterface $entity_type, LanguageManagerInterface $language_manager, ContentTranslationManagerInterface $manager, EntityManagerInterface $entity_manager, AccountInterface $current_user) {
public function __construct(EntityTypeInterface $entity_type, LanguageManagerInterface $language_manager, ContentTranslationManagerInterface $manager, EntityManagerInterface $entity_manager, AccountInterface $current_user, MessengerInterface $messenger) {
$this->entityTypeId = $entity_type->id();
$this->entityType = $entity_type;
$this->languageManager = $language_manager;
$this->manager = $manager;
$this->entityTypeManager = $entity_manager;
$this->currentUser = $current_user;
$this->fieldStorageDefinitions = $entity_manager->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
$this->messenger = $messenger;
}
/**
@ -102,7 +126,8 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
$container->get('language_manager'),
$container->get('content_translation.manager'),
$container->get('entity.manager'),
$container->get('current_user')
$container->get('current_user'),
$container->get('messenger')
);
}
@ -116,6 +141,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
->setLabel(t('Translation source'))
->setDescription(t('The source language from which this translation was created.'))
->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED)
->setInitialValue(LanguageInterface::LANGCODE_NOT_SPECIFIED)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
@ -123,6 +149,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
->setLabel(t('Translation outdated'))
->setDescription(t('A boolean indicating whether this translation needs to be updated.'))
->setDefaultValue(FALSE)
->setInitialValue(FALSE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
@ -142,6 +169,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
->setLabel(t('Translation status'))
->setDescription(t('A boolean indicating whether the translation is visible to non-translators.'))
->setDefaultValue(TRUE)
->setInitialValue(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
}
@ -266,6 +294,8 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
* {@inheritdoc}
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$form_object = $form_state->getFormObject();
$form_langcode = $form_object->getFormLangcode($form_state);
$entity_langcode = $entity->getUntranslated()->language()->getId();
@ -360,7 +390,12 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
break;
}
}
$access = $this->getTranslationAccess($entity, 'delete')->isAllowed() || ($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form'));
/** @var \Drupal\Core\Access\AccessResultInterface $delete_access */
$delete_access = \Drupal::service('content_translation.delete_access')->checkAccess($entity);
$access = $delete_access->isAllowed() && (
$this->getTranslationAccess($entity, 'delete')->isAllowed() ||
($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form'))
);
$form['actions']['delete_translation'] = [
'#type' => 'submit',
'#value' => t('Delete translation'),
@ -423,12 +458,19 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
];
$translate = !$new_translation && $metadata->isOutdated();
if (!$translate) {
$outdated_access = !ContentTranslationManager::isPendingRevisionSupportEnabled($entity->getEntityTypeId(), $entity->bundle());
if (!$outdated_access) {
$form['content_translation']['outdated'] = [
'#markup' => $this->t('Translations cannot be flagged as outdated when content is moderated.'),
];
}
elseif (!$translate) {
$form['content_translation']['retranslate'] = [
'#type' => 'checkbox',
'#title' => t('Flag other translations as outdated'),
'#default_value' => FALSE,
'#description' => t('If you made a significant change, which means the other translations should be updated, you can flag all translations of this content as outdated. This will not change any other property of them, like whether they are published or not.'),
'#access' => $outdated_access,
];
}
else {
@ -437,6 +479,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
'#title' => t('This translation needs to be updated'),
'#default_value' => $translate,
'#description' => t('When this option is checked, this translation needs to be updated. Uncheck when the translation is up to date again.'),
'#access' => $outdated_access,
];
$form['content_translation']['#open'] = TRUE;
}
@ -469,10 +512,6 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
'#default_value' => $new_translation || !$date ? '' : format_date($date, 'custom', 'Y-m-d H:i:s O'),
];
if (isset($language_widget)) {
$language_widget['#multilingual'] = TRUE;
}
$form['#process'][] = [$this, 'entityFormSharedElements'];
}
@ -509,6 +548,20 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
$ignored_types = array_flip(['actions', 'value', 'hidden', 'vertical_tabs', 'token', 'details']);
}
/** @var \Drupal\Core\Entity\ContentEntityForm $form_object */
$form_object = $form_state->getFormObject();
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $form_object->getEntity();
$display_translatability_clue = !$entity->isDefaultTranslationAffectedOnly();
$hide_untranslatable_fields = $entity->isDefaultTranslationAffectedOnly() && !$entity->isDefaultTranslation();
$translation_form = $form_state->get(['content_translation', 'translation_form']);
$display_warning = FALSE;
// We use field definitions to identify untranslatable field widgets to be
// hidden. Fields that are not involved in translation changes checks should
// not be affected by this logic (the "revision_log" field, for instance).
$field_definitions = array_diff_key($entity->getFieldDefinitions(), array_flip($this->getFieldsToSkipFromTranslationChangesCheck($entity)));
foreach (Element::children($element) as $key) {
if (!isset($element[$key]['#type'])) {
$this->entityFormSharedElements($element[$key], $form_state, $form);
@ -521,10 +574,17 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
// Elements are considered to be non multilingual by default.
if (empty($element[$key]['#multilingual'])) {
// If we are displaying a multilingual entity form we need to provide
// translatability clues, otherwise the shared form elements should be
// hidden.
if (!$form_state->get(['content_translation', 'translation_form'])) {
$this->addTranslatabilityClue($element[$key]);
// translatability clues, otherwise the non-multilingual form elements
// should be hidden.
if (!$translation_form) {
if ($display_translatability_clue) {
$this->addTranslatabilityClue($element[$key]);
}
// Hide widgets for untranslatable fields.
if ($hide_untranslatable_fields && isset($field_definitions[$key])) {
$element[$key]['#access'] = FALSE;
$display_warning = TRUE;
}
}
else {
$element[$key]['#access'] = FALSE;
@ -533,6 +593,11 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
}
}
if ($display_warning && !$form_state->isSubmitted() && !$form_state->isRebuilding()) {
$url = $entity->getUntranslated()->toUrl('edit-form')->toString();
$this->messenger->addWarning($this->t('Fields that apply to all languages are hidden to avoid conflicting changes. <a href=":url">Edit them on the original language form</a>.', [':url' => $url]));
}
return $element;
}
@ -640,7 +705,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
// after the entity has been validated, so that it does not break the
// EntityChanged constraint validator. The content translation metadata
// field for the changed timestamp does not have such a constraint defined
// at the moment, but it is correct to update it's value in a submission
// at the moment, but it is correct to update its value in a submission
// handler as well and have the same logic like in the Form API.
if ($entity->hasField('content_translation_changed')) {
$metadata = $this->manager->getTranslationMetadata($entity);
@ -665,7 +730,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
'target' => $form_object->getFormLangcode($form_state),
]);
$languages = $this->languageManager->getLanguages();
drupal_set_message(t('Source language set to: %language', ['%language' => $languages[$source]->getName()]));
$this->messenger->addStatus(t('Source language set to: %language', ['%language' => $languages[$source]->getName()]));
}
/**
@ -674,10 +739,10 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
* Takes care of entity deletion.
*/
public function entityFormDelete($form, FormStateInterface $form_state) {
$form_object = $form_state->getFormObject()->getEntity();
$form_object = $form_state->getFormObject();
$entity = $form_object->getEntity();
if (count($entity->getTranslationLanguages()) > 1) {
drupal_set_message(t('This will delete all the translations of %label.', ['%label' => $entity->label()]), 'warning');
$this->messenger->addWarning(t('This will delete all the translations of %label.', ['%label' => $entity->label()]));
}
}

View file

@ -4,11 +4,12 @@ namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\workflows\Entity\Workflow;
/**
* Provides common functionality for content translation.
*/
class ContentTranslationManager implements ContentTranslationManagerInterface {
class ContentTranslationManager implements ContentTranslationManagerInterface, BundleTranslationSettingsInterface {
/**
* The entity type manager.
@ -105,6 +106,23 @@ class ContentTranslationManager implements ContentTranslationManagerInterface {
return $enabled;
}
/**
* {@inheritdoc}
*/
public function setBundleTranslationSettings($entity_type_id, $bundle, array $settings) {
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
$config->setThirdPartySetting('content_translation', 'bundle_settings', $settings)
->save();
}
/**
* {@inheritdoc}
*/
public function getBundleTranslationSettings($entity_type_id, $bundle) {
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
return $config->getThirdPartySetting('content_translation', 'bundle_settings', []);
}
/**
* Loads a content language config entity based on the entity type and bundle.
*
@ -128,4 +146,47 @@ class ContentTranslationManager implements ContentTranslationManagerInterface {
return $config;
}
/**
* Checks whether support for pending revisions should be enabled.
*
* @param string $entity_type_id
* The ID of the entity type to be checked.
* @param string $bundle_id
* (optional) The ID of the bundle to be checked. Defaults to none.
*
* @return bool
* TRUE if pending revisions should be enabled, FALSE otherwise.
*
* @internal
* There is ongoing discussion about how pending revisions should behave.
* The logic enabling pending revision support is likely to change once a
* decision is made.
*
* @see https://www.drupal.org/node/2940575
*/
public static function isPendingRevisionSupportEnabled($entity_type_id, $bundle_id = NULL) {
if (!\Drupal::moduleHandler()->moduleExists('content_moderation')) {
return FALSE;
}
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
$plugin = $workflow->getTypePlugin();
$entity_type_ids = array_flip($plugin->getEntityTypes());
if (isset($entity_type_ids[$entity_type_id])) {
if (!isset($bundle_id)) {
return TRUE;
}
else {
$bundle_ids = array_flip($plugin->getBundlesForEntityType($entity_type_id));
if (isset($bundle_ids[$bundle_id])) {
return TRUE;
}
}
}
}
return FALSE;
}
}

View file

@ -27,7 +27,7 @@ class ContentTranslationMetadataWrapper implements ContentTranslationMetadataWra
/**
* Initializes an instance of the content translation metadata handler.
*
* @param EntityInterface $translation
* @param \Drupal\Core\Entity\EntityInterface $translation
* The entity translation to be wrapped.
* @param ContentTranslationHandlerInterface $handler
* The content translation handler.

View file

@ -2,6 +2,7 @@
namespace Drupal\content_translation\Controller;
use Drupal\content_translation\ContentTranslationManager;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
@ -87,6 +88,7 @@ class ContentTranslationController extends ControllerBase {
$handler = $this->entityManager()->getHandler($entity_type_id, 'translation');
$manager = $this->manager;
$entity_type = $entity->getEntityType();
$use_latest_revisions = $entity_type->isRevisionable() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle());
// Start collecting the cacheability metadata, starting with the entity and
// later merge in the access result cacheability metadata.
@ -99,6 +101,9 @@ class ContentTranslationController extends ControllerBase {
$rows = [];
$show_source_column = FALSE;
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($entity_type_id);
$default_revision = $storage->load($entity->id());
if ($this->languageManager()->isMultilingual()) {
// Determine whether the current entity is translatable.
@ -121,37 +126,33 @@ class ContentTranslationController extends ControllerBase {
$language_name = $language->getName();
$langcode = $language->getId();
$add_url = new Url(
"entity.$entity_type_id.content_translation_add",
[
'source' => $original,
'target' => $language->getId(),
$entity_type_id => $entity->id(),
],
[
'language' => $language,
]
);
$edit_url = new Url(
"entity.$entity_type_id.content_translation_edit",
[
'language' => $language->getId(),
$entity_type_id => $entity->id(),
],
[
'language' => $language,
]
);
$delete_url = new Url(
"entity.$entity_type_id.content_translation_delete",
[
'language' => $language->getId(),
$entity_type_id => $entity->id(),
],
[
'language' => $language,
]
);
// If the entity type is revisionable, we may have pending revisions
// with translations not available yet in the default revision. Thus we
// need to load the latest translation-affecting revision for each
// language to be sure we are listing all available translations.
if ($use_latest_revisions) {
$entity = $default_revision;
$latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
if ($latest_revision_id) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */
$latest_revision = $storage->loadRevision($latest_revision_id);
// Make sure we do not list removed translations, i.e. translations
// that have been part of a default revision but no longer are.
if (!$latest_revision->wasDefaultRevision() || $default_revision->hasTranslation($langcode)) {
$entity = $latest_revision;
}
}
$translations = $entity->getTranslationLanguages();
}
$options = ['language' => $language];
$add_url = $entity->toUrl('drupal:content-translation-add', $options)
->setRouteParameter('source', $original)
->setRouteParameter('target', $language->getId());
$edit_url = $entity->toUrl('drupal:content-translation-edit', $options)
->setRouteParameter('language', $language->getId());
$delete_url = $entity->toUrl('drupal:content-translation-delete', $options)
->setRouteParameter('language', $language->getId());
$operations = [
'data' => [
'#type' => 'operations',
@ -196,38 +197,50 @@ class ContentTranslationController extends ControllerBase {
if (isset($links['edit'])) {
$links['edit']['title'] = $this->t('Edit');
}
$status = ['data' => [
'#type' => 'inline_template',
'#template' => '<span class="status">{% if status %}{{ "Published"|t }}{% else %}{{ "Not published"|t }}{% endif %}</span>{% if outdated %} <span class="marker">{{ "outdated"|t }}</span>{% endif %}',
'#context' => [
'status' => $metadata->isPublished(),
'outdated' => $metadata->isOutdated(),
$status = [
'data' => [
'#type' => 'inline_template',
'#template' => '<span class="status">{% if status %}{{ "Published"|t }}{% else %}{{ "Not published"|t }}{% endif %}</span>{% if outdated %} <span class="marker">{{ "outdated"|t }}</span>{% endif %}',
'#context' => [
'status' => $metadata->isPublished(),
'outdated' => $metadata->isOutdated(),
],
],
]];
];
if ($is_original) {
$language_name = $this->t('<strong>@language_name (Original language)</strong>', ['@language_name' => $language_name]);
$source_name = $this->t('n/a');
}
else {
$source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a');
$delete_access = $entity->access('delete', NULL, TRUE);
$translation_access = $handler->getTranslationAccess($entity, 'delete');
$cacheability = $cacheability
->merge(CacheableMetadata::createFromObject($delete_access))
->merge(CacheableMetadata::createFromObject($translation_access));
if ($entity->access('delete') && $entity_type->hasLinkTemplate('delete-form')) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => $entity->urlInfo('delete-form'),
'language' => $language,
];
/** @var \Drupal\Core\Access\AccessResultInterface $delete_route_access */
$delete_route_access = \Drupal::service('content_translation.delete_access')->checkAccess($translation);
$cacheability->addCacheableDependency($delete_route_access);
if ($delete_route_access->isAllowed()) {
$source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a');
$delete_access = $entity->access('delete', NULL, TRUE);
$translation_access = $handler->getTranslationAccess($entity, 'delete');
$cacheability
->addCacheableDependency($delete_access)
->addCacheableDependency($translation_access);
if ($delete_access->isAllowed() && $entity_type->hasLinkTemplate('delete-form')) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => $entity->urlInfo('delete-form'),
'language' => $language,
];
}
elseif ($translation_access->isAllowed()) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => $delete_url,
];
}
}
elseif ($translation_access->isAllowed()) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => $delete_url,
];
else {
$this->messenger()->addWarning($this->t('The "Delete translation" action is only available for published translations.'), FALSE);
}
}
}
@ -328,8 +341,21 @@ class ContentTranslationController extends ControllerBase {
* A processed form array ready to be rendered.
*/
public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $route_match->getParameter($entity_type_id);
// In case of a pending revision, make sure we load the latest
// translation-affecting revision for the source language, otherwise the
// initial form values may not be up-to-date.
if (!$entity->isDefaultRevision() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId());
$revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $source->getId());
if ($revision_id != $entity->getRevisionId()) {
$entity = $storage->loadRevision($revision_id);
}
}
// @todo Exploit the upcoming hook_entity_prepare() when available.
// See https://www.drupal.org/node/1810394.
$this->prepareTranslation($entity, $source, $target);

View file

@ -5,6 +5,8 @@ namespace Drupal\content_translation;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
/**
* Provides field translation synchronization capabilities.
@ -18,14 +20,57 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
*/
protected $entityManager;
/**
* The field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* Constructs a FieldTranslationSynchronizer object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
* The entity manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager.
*/
public function __construct(EntityManagerInterface $entityManager) {
public function __construct(EntityManagerInterface $entityManager, FieldTypePluginManagerInterface $field_type_manager) {
$this->entityManager = $entityManager;
$this->fieldTypeManager = $field_type_manager;
}
/**
* {@inheritdoc}
*/
public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition) {
$properties = [];
$settings = $this->getFieldSynchronizationSettings($field_definition);
foreach ($settings as $group => $translatable) {
if (!$translatable) {
$field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType());
if (!empty($field_type_definition['column_groups'][$group]['columns'])) {
$properties = array_merge($properties, $field_type_definition['column_groups'][$group]['columns']);
}
}
}
return $properties;
}
/**
* Returns the synchronization settings for the specified field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* A field definition.
*
* @return string[]
* An array of synchronized field property names.
*/
protected function getFieldSynchronizationSettings(FieldDefinitionInterface $field_definition) {
if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable()) {
return $field_definition->getThirdPartySetting('content_translation', 'translation_sync', []);
}
return [];
}
/**
@ -33,7 +78,6 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
*/
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
@ -43,21 +87,55 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
}
// 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());
$entity_unchanged = $this->getOriginalEntity($entity);
if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) {
return;
}
if ($entity->isNewRevision()) {
if ($entity->isDefaultTranslationAffectedOnly()) {
// If changes to untranslatable fields are configured to affect only the
// default translation, we need to skip synchronization in pending
// revisions, otherwise multiple translations would be affected.
if (!$entity->isDefaultRevision()) {
return;
}
// When this mode is enabled, changes to synchronized properties are
// allowed only in the default translation, thus we need to make sure this
// is always used as source for the synchronization process.
else {
$sync_langcode = $entity->getUntranslated()->language()->getId();
}
}
elseif ($entity->isDefaultRevision()) {
// If a new default revision is being saved, but a newer default
// revision was created meanwhile, use any other translation as source
// for synchronization, since that will have been merged from the
// default revision. In this case the actual language does not matter as
// synchronized properties are the same for all the translations in the
// default revision.
/** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
$default_revision = $this->entityManager
->getStorage($entity->getEntityTypeId())
->load($entity->id());
if ($default_revision->getLoadedRevisionId() !== $entity->getLoadedRevisionId()) {
$other_langcodes = array_diff_key($default_revision->getTranslationLanguages(), [$sync_langcode => FALSE]);
if ($other_langcodes) {
$sync_langcode = key($other_langcodes);
}
}
}
}
/** @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());
$field_type_definition = $this->fieldTypeManager->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')) {
if (($translation_sync = $this->getFieldSynchronizationSettings($field_definition)) && !$items->isEmpty()) {
// Retrieve all the untranslatable column groups and merge them into
// single list.
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
@ -101,10 +179,30 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
}
}
/**
* Returns the original unchanged entity to be used to detect changes.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being changed.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The unchanged entity.
*/
protected function getOriginalEntity(ContentEntityInterface $entity) {
if (!isset($entity->original)) {
$storage = $this->entityManager->getStorage($entity->getEntityTypeId());
$original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
}
else {
$original = $entity->original;
}
return $original;
}
/**
* {@inheritdoc}
*/
public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $columns) {
public function synchronizeItems(array &$values, array $unchanged_items, $sync_langcode, array $translations, array $properties) {
$source_items = $values[$sync_langcode];
// Make sure we can detect any change in the source items.
@ -120,7 +218,7 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
// 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)) {
if ($item_id = $this->itemHash($items, $delta, $properties)) {
$change_map[$item_id][$key][] = $delta;
}
}
@ -153,7 +251,7 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
$old_delta = NULL;
$new_delta = NULL;
if ($item_id = $this->itemHash($source_items, $delta, $columns)) {
if ($item_id = $this->itemHash($source_items, $delta, $properties)) {
if (!empty($change_map[$item_id]['old'])) {
$old_delta = array_shift($change_map[$item_id]['old']);
}
@ -174,9 +272,7 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
// 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;
$values[$langcode][$delta] = $this->createMergedItem($source_items[$delta], $original_field_values[$langcode][$delta], $properties);
}
// If the delta doesn't exist, copy from the source language.
elseif ($created) {
@ -190,13 +286,37 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
// 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;
// When saving a default revision starting from a pending revision,
// we may have desynchronized field values, so we make sure that
// untranslatable properties are synchronized, even if in any other
// situation this would not be necessary.
$values[$langcode][$new_delta] = $this->createMergedItem($source_items[$new_delta], $item, $properties);
}
}
}
}
}
/**
* Creates a merged item.
*
* @param array $source_item
* An item containing the untranslatable properties to be synchronized.
* @param array $target_item
* An item containing the translatable properties to be kept.
* @param string[] $properties
* An array of properties to be synchronized.
*
* @return array
* A merged item array.
*/
protected function createMergedItem(array $source_item, array $target_item, array $properties) {
$property_keys = array_flip($properties);
$item_properties_to_sync = array_intersect_key($source_item, $property_keys);
$item_properties_to_keep = array_diff_key($target_item, $property_keys);
return $item_properties_to_sync + $item_properties_to_keep;
}
/**
* Computes a hash code for the specified item.
*
@ -204,19 +324,19 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
* An array of field items.
* @param int $delta
* The delta identifying the item to be processed.
* @param array $columns
* @param array $properties
* 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) {
protected function itemHash(array $items, $delta, array $properties) {
$values = [];
if (isset($items[$delta])) {
foreach ($columns as $column) {
if (!empty($items[$delta][$column])) {
$value = $items[$delta][$column];
foreach ($properties as $property) {
if (!empty($items[$delta][$property])) {
$value = $items[$delta][$property];
// 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));

View file

@ -3,6 +3,7 @@
namespace Drupal\content_translation;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
/**
* Provides field translation synchronization capabilities.
@ -49,9 +50,20 @@ interface FieldTranslationSynchronizerInterface {
* The language code of the items to use as source values.
* @param array $translations
* An array of all the available language codes for the given field.
* @param array $columns
* An array of column names to be synchronized.
* @param array $properties
* An array of property names to be synchronized.
*/
public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $columns);
public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $properties);
/**
* Returns the synchronized properties for the specified field definition.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* A field definition.
*
* @return string[]
* An array of synchronized field property names.
*/
public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition);
}

View file

@ -8,6 +8,8 @@ use Drupal\Core\Language\LanguageInterface;
/**
* Delete translation form for content_translation module.
*
* @internal
*/
class ContentTranslationDeleteForm extends ContentEntityDeleteForm {

View file

@ -0,0 +1,25 @@
<?php
namespace Drupal\content_translation\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Validation constraint for the entity changed timestamp.
*
* @internal
*
* @Constraint(
* id = "ContentTranslationSynchronizedFields",
* label = @Translation("Content translation synchronized fields", context = "Validation"),
* type = {"entity"}
* )
*/
class ContentTranslationSynchronizedFieldsConstraint extends Constraint {
// In this case "elements" refers to "field properties", in fact it is what we
// are using in the UI elsewhere.
public $defaultRevisionMessage = 'Non-translatable field elements can only be changed when updating the current revision.';
public $defaultTranslationMessage = 'Non-translatable field elements can only be changed when updating the original language.';
}

View file

@ -0,0 +1,226 @@
<?php
namespace Drupal\content_translation\Plugin\Validation\Constraint;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\content_translation\FieldTranslationSynchronizerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Checks that synchronized fields are handled correctly in pending revisions.
*
* As for untranslatable fields, two modes are supported:
* - When changes to untranslatable fields are configured to affect all revision
* translations, synchronized field properties can be changed only in default
* revisions.
* - When changes to untranslatable fields affect are configured to affect only
* the revision's default translation, synchronized field properties can be
* changed only when editing the default translation. This may lead to
* temporarily desynchronized values, when saving a pending revision for the
* default translation that changes a synchronized property. These are
* actually synchronized when saving changes to the default translation as a
* new default revision.
*
* @see \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint
* @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraintValidator
*
* @internal
*/
class ContentTranslationSynchronizedFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* The field translation synchronizer.
*
* @var \Drupal\content_translation\FieldTranslationSynchronizerInterface
*/
protected $synchronizer;
/**
* ContentTranslationSynchronizedFieldsConstraintValidator constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* The content translation manager.
* @param \Drupal\content_translation\FieldTranslationSynchronizerInterface $synchronizer
* The field translation synchronizer.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ContentTranslationManagerInterface $content_translation_manager, FieldTranslationSynchronizerInterface $synchronizer) {
$this->entityTypeManager = $entity_type_manager;
$this->contentTranslationManager = $content_translation_manager;
$this->synchronizer = $synchronizer;
}
/**
* [@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('content_translation.manager'),
$container->get('content_translation.synchronizer')
);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
/** @var \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint $constraint */
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $value;
if ($entity->isNew() || !$entity->getEntityType()->isRevisionable()) {
return;
}
// When changes to untranslatable fields are configured to affect all
// revision translations, we always allow changes in default revisions.
if ($entity->isDefaultRevision() && !$entity->isDefaultTranslationAffectedOnly()) {
return;
}
$entity_type_id = $entity->getEntityTypeId();
if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
return;
}
$synchronized_properties = $this->getSynchronizedPropertiesByField($entity->getFieldDefinitions());
if (!$synchronized_properties) {
return;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $original */
$original = $this->getOriginalEntity($entity);
$original_translation = $this->getOriginalTranslation($entity, $original);
if ($this->hasSynchronizedPropertyChanges($entity, $original_translation, $synchronized_properties)) {
if ($entity->isDefaultTranslationAffectedOnly()) {
foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
$this->context->addViolation($constraint->defaultTranslationMessage);
break;
}
}
}
else {
$this->context->addViolation($constraint->defaultRevisionMessage);
}
}
}
/**
* Checks whether any synchronized property has changes.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being validated.
* @param \Drupal\Core\Entity\ContentEntityInterface $original
* The original unchanged entity.
* @param string[][] $synchronized_properties
* An associative array of arrays of synchronized field properties keyed by
* field name.
*
* @return bool
* TRUE if changes in synchronized properties were detected, FALSE
* otherwise.
*/
protected function hasSynchronizedPropertyChanges(ContentEntityInterface $entity, ContentEntityInterface $original, array $synchronized_properties) {
foreach ($synchronized_properties as $field_name => $properties) {
foreach ($properties as $property) {
$items = $entity->get($field_name)->getValue();
$original_items = $original->get($field_name)->getValue();
if (count($items) !== count($original_items)) {
return TRUE;
}
foreach ($items as $delta => $item) {
// @todo This loose comparison is not fully reliable. Revisit this
// after https://www.drupal.org/project/drupal/issues/2941092.
if ($items[$delta][$property] != $original_items[$delta][$property]) {
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Returns the original unchanged entity to be used to detect changes.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being changed.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The unchanged entity.
*/
protected function getOriginalEntity(ContentEntityInterface $entity) {
if (!isset($entity->original)) {
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
$original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
}
else {
$original = $entity->original;
}
return $original;
}
/**
* Returns the original translation.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being validated.
* @param \Drupal\Core\Entity\ContentEntityInterface $original
* The original entity.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The original entity translation object.
*/
protected function getOriginalTranslation(ContentEntityInterface $entity, ContentEntityInterface $original) {
$langcode = $entity->language()->getId();
if ($original->hasTranslation($langcode)) {
$original_langcode = $langcode;
}
else {
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
$original_langcode = $metadata->getSource();
}
return $original->getTranslation($original_langcode);
}
/**
* Returns the synchronized properties for every specified field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
* An array of field definitions.
*
* @return string[][]
* An associative array of arrays of field property names keyed by field
* name.
*/
public function getSynchronizedPropertiesByField(array $field_definitions) {
$synchronizer = $this->synchronizer;
$synchronized_properties = array_filter(array_map(
function (FieldDefinitionInterface $field_definition) use ($synchronizer) {
return $synchronizer->getFieldSynchronizedProperties($field_definition);
},
$field_definitions
));
return $synchronized_properties;
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Drupal\content_translation\Plugin\migrate\source;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
/**
* Gets an i18n translation from the source database.
*/
trait I18nQueryTrait {
/**
* The i18n string table name.
*
* @var string
*/
protected $i18nStringTable;
/**
* Gets the translation for the property not already in the row.
*
* For some i18n migrations there are two translation values, such as a
* translated title and a translated description, that need to be retrieved.
* Since these values are stored in separate rows of the i18nStringTable
* table we get them individually, one in the source plugin query() and the
* other in prepareRow(). The names of the properties varies, for example,
* in BoxTranslation they are 'body' and 'title' whereas in
* MenuLinkTranslation they are 'title' and 'description'. This will save both
* translations to the row.
*
* @param \Drupal\migrate\Row $row
* The current migration row which must include both a 'language' property
* and an 'objectid' property. The 'objectid' is the value for the
* 'objectid' field in the i18n_string table.
* @param string $property_not_in_row
* The name of the property to get the translation for.
* @param string $object_id_name
* The value of the objectid in the i18n table.
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map
* The ID map.
*
* @return bool
* FALSE if the property has already been migrated.
*
* @throws \Drupal\migrate\MigrateException
*/
protected function getPropertyNotInRowTranslation(Row $row, $property_not_in_row, $object_id_name, MigrateIdMapInterface $id_map) {
$language = $row->getSourceProperty('language');
if (!$language) {
throw new MigrateException('No language found.');
}
$object_id = $row->getSourceProperty($object_id_name);
if (!$object_id) {
throw new MigrateException('No objectid found.');
}
// If this row has been migrated it is a duplicate so skip it.
if ($id_map->lookupDestinationIds([$object_id_name => $object_id, 'language' => $language])) {
return FALSE;
}
// Save the translation for the property already in the row.
$property_in_row = $row->getSourceProperty('property');
$row->setSourceProperty($property_in_row . '_translated', $row->getSourceProperty('translation'));
// Get the translation, if one exists, for the property not already in the
// row.
$query = $this->select($this->i18nStringTable, 'i18n')
->fields('i18n', ['lid'])
->condition('i18n.property', $property_not_in_row)
->condition('i18n.objectid', $object_id);
$query->leftJoin('locales_target', 'lt', 'i18n.lid = lt.lid');
$query->condition('lt.language', $language);
$query->addField('lt', 'translation');
$results = $query->execute()->fetchAssoc();
if (!$results) {
$row->setSourceProperty($property_not_in_row . '_translated', NULL);
}
else {
$row->setSourceProperty($property_not_in_row . '_translated', $results['translation']);
}
return TRUE;
}
}

View file

@ -0,0 +1,198 @@
<?php
namespace Drupal\content_translation\Plugin\migrate\source\d7;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 Entity Translation settings from variables.
*
* @MigrateSource(
* id = "d7_entity_translation_settings",
* source_module = "entity_translation"
* )
*/
class EntityTranslationSettings extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
// Query all meaningful variables for entity translation.
$query = $this->select('variable', 'v')
->fields('v', ['name', 'value']);
$condition = $query->orConditionGroup()
// The 'entity_translation_entity_types' variable tells us which entity
// type uses entity translation.
->condition('name', 'entity_translation_entity_types')
// The 'entity_translation_taxonomy' variable tells us which taxonomy
// vocabulary uses entity_translation.
->condition('name', 'entity_translation_taxonomy')
// The 'entity_translation_settings_%' variables give us the entity
// translation settings for each entity type and each bundle.
->condition('name', 'entity_translation_settings_%', 'LIKE')
// The 'language_content_type_%' variables tells us which node type and
// which comment type uses entity translation.
->condition('name', 'language_content_type_%', 'LIKE');
$query->condition($condition);
return $query;
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$results = array_map('unserialize', $this->prepareQuery()->execute()->fetchAllKeyed());
$rows = [];
// Find out which entity type uses entity translation by looking at the
// 'entity_translation_entity_types' variable.
$entity_types = array_filter($results['entity_translation_entity_types']);
// If no entity type uses entity translation, there's nothing to do.
if (empty($entity_types)) {
return new \ArrayIterator($rows);
}
// Find out which node type uses entity translation by looking at the
// 'language_content_type_%' variables.
$node_types = [];
foreach ($results as $name => $value) {
if (preg_match('/^language_content_type_(.+)$/', $name, $matches) && (int) $value === 4) {
$node_types[] = $matches[1];
}
}
// Find out which vocabulary uses entity translation by looking at the
// 'entity_translation_taxonomy' variable.
$vocabularies = [];
if (isset($results['entity_translation_taxonomy']) && is_array($results['entity_translation_taxonomy'])) {
$vocabularies = array_keys(array_filter($results['entity_translation_taxonomy']));
}
if (in_array('node', $entity_types, TRUE) && !empty($node_types)) {
// For each node type that uses entity translation, check if a
// settings variable exists for that node type, otherwise use default
// values.
foreach ($node_types as $node_type) {
$settings = isset($results['entity_translation_settings_node__' . $node_type]) ? $results['entity_translation_settings_node__' . $node_type] : [];
$rows[] = [
'id' => 'node.' . $node_type,
'target_entity_type_id' => 'node',
'target_bundle' => $node_type,
'default_langcode' => isset($settings['default_language']) ? $settings['default_language'] : 'und',
// The Drupal 7 'hide_language_selector' configuration has become
// 'language_alterable' in Drupal 8 so we need to negate the value we
// receive from the source. The Drupal 7 'hide_language_selector'
// default value for the node entity type was FALSE so in Drupal 8 it
// should be set to TRUE, unlike the other entity types for which
// it's the opposite.
'language_alterable' => isset($settings['hide_language_selector']) ? (bool) !$settings['hide_language_selector'] : TRUE,
'untranslatable_fields_hide' => isset($settings['shared_fields_original_only']) ? (bool) $settings['shared_fields_original_only'] : FALSE,
];
}
}
if (in_array('comment', $entity_types, TRUE) && !empty($node_types)) {
// A comment type uses entity translation if the associated node type
// uses it. So, for each node type that uses entity translation, check
// if a settings variable exists for that comment type, otherwise use
// default values.
foreach ($node_types as $node_type) {
$settings = isset($results['entity_translation_settings_comment__comment_node_' . $node_type]) ? $results['entity_translation_settings_comment__comment_node_' . $node_type] : [];
// Forum uses a hardcoded comment type name, so make sure we use it
// when we're dealing with forum comment type.
$bundle = $node_type == 'forum' ? 'comment_forum' : 'comment_node_' . $node_type;
$rows[] = [
'id' => 'comment.' . $bundle,
'target_entity_type_id' => 'comment',
'target_bundle' => $bundle,
'default_langcode' => isset($settings['default_language']) ? $settings['default_language'] : 'xx-et-current',
// The Drupal 7 'hide_language_selector' configuration has become
// 'language_alterable' in Drupal 8 so we need to negate the value we
// receive from the source. The Drupal 7 'hide_language_selector'
// default value for the comment entity type was TRUE so in Drupal 8
// it should be set to FALSE.
'language_alterable' => isset($settings['hide_language_selector']) ? (bool) !$settings['hide_language_selector'] : FALSE,
'untranslatable_fields_hide' => isset($settings['shared_fields_original_only']) ? (bool) $settings['shared_fields_original_only'] : FALSE,
];
}
}
if (in_array('taxonomy_term', $entity_types, TRUE) && !empty($vocabularies)) {
// For each vocabulary that uses entity translation, check if a
// settings variable exists for that vocabulary, otherwise use default
// values.
foreach ($vocabularies as $vocabulary) {
$settings = isset($results['entity_translation_settings_taxonomy_term__' . $vocabulary]) ? $results['entity_translation_settings_taxonomy_term__' . $vocabulary] : [];
$rows[] = [
'id' => 'taxonomy_term.' . $vocabulary,
'target_entity_type_id' => 'taxonomy_term',
'target_bundle' => $vocabulary,
'default_langcode' => isset($settings['default_language']) ? $settings['default_language'] : 'xx-et-default',
// The Drupal 7 'hide_language_selector' configuration has become
// 'language_alterable' in Drupal 8 so we need to negate the value we
// receive from the source. The Drupal 7 'hide_language_selector'
// default value for the taxonomy_term entity type was TRUE so in
// Drupal 8 it should be set to FALSE.
'language_alterable' => isset($settings['hide_language_selector']) ? (bool) !$settings['hide_language_selector'] : FALSE,
'untranslatable_fields_hide' => isset($settings['shared_fields_original_only']) ? (bool) $settings['shared_fields_original_only'] : FALSE,
];
}
}
if (in_array('user', $entity_types, TRUE)) {
// User entity type is not bundleable. Check if a settings variable
// exists, otherwise use default values.
$settings = isset($results['entity_translation_settings_user__user']) ? $results['entity_translation_settings_user__user'] : [];
$rows[] = [
'id' => 'user.user',
'target_entity_type_id' => 'user',
'target_bundle' => 'user',
'default_langcode' => isset($settings['default_language']) ? $settings['default_language'] : 'xx-et-default',
// The Drupal 7 'hide_language_selector' configuration has become
// 'language_alterable' in Drupal 8 so we need to negate the value we
// receive from the source. The Drupal 7 'hide_language_selector'
// default value for the user entity type was TRUE so in Drupal 8 it
// should be set to FALSE.
'language_alterable' => isset($settings['hide_language_selector']) ? (bool) !$settings['hide_language_selector'] : FALSE,
'untranslatable_fields_hide' => isset($settings['shared_fields_original_only']) ? (bool) $settings['shared_fields_original_only'] : FALSE,
];
}
return new \ArrayIterator($rows);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'id' => $this->t('The configuration ID'),
'target_entity_type_id' => $this->t('The target entity type ID'),
'target_bundle' => $this->t('The target bundle'),
'default_langcode' => $this->t('The default language'),
'language_alterable' => $this->t('Whether to show language selector on create and edit pages'),
'untranslatable_fields_hide' => $this->t('Whether to hide non translatable fields on translation forms'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['id']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function count($refresh = FALSE) {
// Since the number of variables we fetch with query() does not match the
// actual number of rows generated by initializeIterator(), we need to
// override count() to return the correct count.
return (int) $this->initializeIterator()->count();
}
}

View file

@ -2,6 +2,7 @@
namespace Drupal\content_translation\Routing;
use Drupal\content_translation\ContentTranslationManager;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
@ -35,18 +36,6 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->contentTranslationManager->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
// Try to get the route from the current collection.
$link_template = $entity_type->getLinkTemplate('canonical');
if (strpos($link_template, '/') !== FALSE) {
$base_path = '/' . $link_template;
}
else {
if (!$entity_route = $collection->get("entity.$entity_type_id.canonical")) {
continue;
}
$base_path = $entity_route->getPath();
}
// Inherit admin route status from edit route, if exists.
$is_admin = FALSE;
$route_name = "entity.$entity_type_id.edit_form";
@ -54,110 +43,130 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
$is_admin = (bool) $edit_route->getOption('_admin_route');
}
$path = $base_path . '/translations';
$load_latest_revision = ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id);
$route = new Route(
$path,
[
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::overview',
'entity_type_id' => $entity_type_id,
],
[
'_entity_access' => $entity_type_id . '.view',
'_access_content_translation_overview' => $entity_type_id,
],
[
'parameters' => [
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
],
if ($entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
$route = new Route(
$entity_type->getLinkTemplate('drupal:content-translation-overview'),
[
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::overview',
'entity_type_id' => $entity_type_id,
],
'_admin_route' => $is_admin,
]
);
$route_name = "entity.$entity_type_id.content_translation_overview";
$collection->add($route_name, $route);
$route = new Route(
$path . '/add/{source}/{target}',
[
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::add',
'source' => NULL,
'target' => NULL,
'_title' => 'Add',
'entity_type_id' => $entity_type_id,
],
[
'_entity_access' => $entity_type_id . '.view',
'_access_content_translation_manage' => 'create',
],
[
'parameters' => [
'source' => [
'type' => 'language',
],
'target' => [
'type' => 'language',
],
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
],
[
'_entity_access' => $entity_type_id . '.view',
'_access_content_translation_overview' => $entity_type_id,
],
'_admin_route' => $is_admin,
]
);
$collection->add("entity.$entity_type_id.content_translation_add", $route);
[
'parameters' => [
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
'load_latest_revision' => $load_latest_revision,
],
],
'_admin_route' => $is_admin,
]
);
$route_name = "entity.$entity_type_id.content_translation_overview";
$collection->add($route_name, $route);
}
$route = new Route(
$path . '/edit/{language}',
[
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::edit',
'language' => NULL,
'_title' => 'Edit',
'entity_type_id' => $entity_type_id,
],
[
'_access_content_translation_manage' => 'update',
],
[
'parameters' => [
'language' => [
'type' => 'language',
],
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
],
],
'_admin_route' => $is_admin,
]
);
$collection->add("entity.$entity_type_id.content_translation_edit", $route);
if ($entity_type->hasLinkTemplate('drupal:content-translation-add')) {
$route = new Route(
$entity_type->getLinkTemplate('drupal:content-translation-add'),
[
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::add',
'source' => NULL,
'target' => NULL,
'_title' => 'Add',
'entity_type_id' => $entity_type_id,
$route = new Route(
$path . '/delete/{language}',
[
'_entity_form' => $entity_type_id . '.content_translation_deletion',
'language' => NULL,
'_title' => 'Delete',
'entity_type_id' => $entity_type_id,
],
[
'_access_content_translation_manage' => 'delete',
],
[
'parameters' => [
'language' => [
'type' => 'language',
],
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
],
],
'_admin_route' => $is_admin,
]
);
$collection->add("entity.$entity_type_id.content_translation_delete", $route);
[
'_entity_access' => $entity_type_id . '.view',
'_access_content_translation_manage' => 'create',
],
[
'parameters' => [
'source' => [
'type' => 'language',
],
'target' => [
'type' => 'language',
],
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
'load_latest_revision' => $load_latest_revision,
],
],
'_admin_route' => $is_admin,
]
);
$collection->add("entity.$entity_type_id.content_translation_add", $route);
}
if ($entity_type->hasLinkTemplate('drupal:content-translation-edit')) {
$route = new Route(
$entity_type->getLinkTemplate('drupal:content-translation-edit'),
[
'_controller' => '\Drupal\content_translation\Controller\ContentTranslationController::edit',
'language' => NULL,
'_title' => 'Edit',
'entity_type_id' => $entity_type_id,
],
[
'_access_content_translation_manage' => 'update',
],
[
'parameters' => [
'language' => [
'type' => 'language',
],
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
'load_latest_revision' => $load_latest_revision,
],
],
'_admin_route' => $is_admin,
]
);
$collection->add("entity.$entity_type_id.content_translation_edit", $route);
}
if ($entity_type->hasLinkTemplate('drupal:content-translation-delete')) {
$route = new Route(
$entity_type->getLinkTemplate('drupal:content-translation-delete'),
[
'_entity_form' => $entity_type_id . '.content_translation_deletion',
'language' => NULL,
'_title' => 'Delete',
'entity_type_id' => $entity_type_id,
],
[
'_access_content_translation_manage' => 'delete',
],
[
'parameters' => [
'language' => [
'type' => 'language',
],
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
'load_latest_revision' => $load_latest_revision,
],
],
'_admin_route' => $is_admin,
]
);
$collection->add("entity.$entity_type_id.content_translation_delete", $route);
}
// Add our custom translation deletion access checker.
if ($load_latest_revision) {
$entity_delete_route = $collection->get("entity.$entity_type_id.delete_form");
if ($entity_delete_route) {
$entity_delete_route->addRequirements(['_access_content_translation_delete' => "$entity_type_id.delete"]);
}
}
}
}

View file

@ -9,7 +9,7 @@ use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
/**
@ -37,8 +37,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
protected $testLanguageSelector = TRUE;
/**
* Flag that tells whether the HTML escaping of all languages works or not
* after SafeMarkup change.
* Flag to determine if "all languages" rendering is tested.
*
* @var bool
*/
@ -113,12 +112,11 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
'target' => $langcode,
], ['language' => $language]);
$this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode));
// Assert that HTML is escaped in "all languages" in UI after SafeMarkup
// change.
// Assert that HTML is not escaped unexpectedly.
if ($this->testHTMLEscapeForAllLanguages) {
$this->assertNoRaw('&lt;span class=&quot;translation-entity-all-languages&quot;&gt;(all languages)&lt;/span&gt;');
$this->assertRaw('<span class="translation-entity-all-languages">(all languages)</span>');
@ -141,10 +139,10 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid';
if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) {
$this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(),
SafeMarkup::format('Author of the target translation @langcode correctly stored for translatable owner field.', ['@langcode' => $langcode]));
new FormattableMarkup('Author of the target translation @langcode correctly stored for translatable owner field.', ['@langcode' => $langcode]));
$this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(),
SafeMarkup::format('Author of the target translation @target different from the author of the source translation @source for translatable owner field.',
new FormattableMarkup('Author of the target translation @target different from the author of the source translation @source for translatable owner field.',
['@target' => $langcode, '@source' => $default_langcode]));
}
else {
@ -154,7 +152,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created';
if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) {
$this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(),
SafeMarkup::format('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.',
new FormattableMarkup('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.',
['@target' => $langcode, '@source' => $default_langcode]));
}
else {
@ -178,7 +176,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
'target' => $langcode,
], ['language' => $language]);
// This does not save anything, it merely reloads the form and fills in the
// fields with the values from the different source language.
@ -192,7 +190,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $source_langcode,
'target' => $langcode
'target' => $langcode,
], ['language' => $language]);
$this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode));
$storage->resetCache([$this->entityId]);

View file

@ -5,6 +5,6 @@ package: Testing
version: VERSION
core: 8.x
dependencies:
- content_translation
- language
- entity_test
- drupal:content_translation
- drupal:language
- drupal:entity_test

View file

@ -5,7 +5,39 @@
* Helper module for the Content Translation tests.
*/
use \Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Implements hook_entity_bundle_info_alter().
*/
function content_translation_test_entity_bundle_info_alter(&$bundles) {
// Store the initial status of the "translatable" property for the
// "entity_test_mul" bundle.
$translatable = !empty($bundles['entity_test_mul']['entity_test_mul']['translatable']);
\Drupal::state()->set('content_translation_test.translatable', $translatable);
// Make it translatable if Content Translation did not. This will make the
// entity object translatable even if it is disabled in Content Translation
// settings.
if (!$translatable) {
$bundles['entity_test_mul']['entity_test_mul']['translatable'] = TRUE;
}
}
/**
* Implements hook_entity_access().
*/
function content_translation_test_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
$access = \Drupal::state()->get('content_translation.entity_access.' . $entity->getEntityTypeId());
if (!empty($access[$operation])) {
return AccessResult::allowed();
}
else {
return AccessResult::neutral();
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter().

View file

@ -5,5 +5,5 @@ package: Testing
version: VERSION
core: 8.x
dependencies:
- content_translation
- views
- drupal:content_translation
- drupal:views

View file

@ -1,6 +1,6 @@
<?php
namespace Drupal\content_translation\Tests;
namespace Drupal\Tests\content_translation\Functional;
/**
* Tests the test content translation UI with the test entity.

View file

@ -1,19 +1,18 @@
<?php
namespace Drupal\content_translation\Tests;
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\field\Entity\FieldConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that contextual links are available for content translation.
*
* @group content_translation
*/
class ContentTranslationContextualLinksTest extends WebTestBase {
class ContentTranslationContextualLinksTest extends BrowserTestBase {
/**
* The bundle being tested.
@ -118,60 +117,11 @@ class ContentTranslationContextualLinksTest extends WebTestBase {
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
$this->drupalLogout();
// Check that the translate link appears on the node page.
// Check that the link leads to the translate page.
$this->drupalLogin($this->translator);
$translate_link = 'node/' . $node->id() . '/translations';
$response = $this->renderContextualLinks(['node:node=1:'], 'node/' . $node->id());
$this->assertResponse(200);
$json = Json::decode($response);
$this->setRawContent($json['node:node=1:']);
$this->assertLinkByHref($translate_link, 0, 'The contextual link to translate the node is shown.');
// Check that the link leads to the translate page.
$this->drupalGet($translate_link);
$this->assertRaw(t('Translations of %label', ['%label' => $node->label()]), 'The contextual link leads to the translate page.');
}
/**
* Get server-rendered contextual links for the given contextual link ids.
*
* Copied from \Drupal\contextual\Tests\ContextualDynamicContextTest::renderContextualLinks().
*
* @param array $ids
* An array of contextual link ids.
* @param string $current_path
* The Drupal path for the page for which the contextual links are rendered.
*
* @return string
* The response body.
*/
protected function renderContextualLinks($ids, $current_path) {
// Build POST values.
$post = [];
for ($i = 0; $i < count($ids); $i++) {
$post['ids[' . $i . ']'] = $ids[$i];
}
// Serialize POST values.
foreach ($post as $key => $value) {
// Encode according to application/x-www-form-urlencoded
// Both names and values needs to be urlencoded, according to
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
$post[$key] = urlencode($key) . '=' . urlencode($value);
}
$post = implode('&', $post);
// Perform HTTP request.
return $this->curlExec([
CURLOPT_URL => \Drupal::url('contextual.render', [], ['absolute' => TRUE, 'query' => ['destination' => $current_path]]),
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $post,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
],
]);
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Drupal\content_translation\Tests;
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Tests\BrowserTestBase;

View file

@ -1,15 +1,15 @@
<?php
namespace Drupal\content_translation\Tests;
namespace Drupal\Tests\content_translation\Functional;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Test enabling content translation module.
*
* @group content_translation
*/
class ContentTranslationEnableTest extends WebTestBase {
class ContentTranslationEnableTest extends BrowserTestBase {
/**
* {@inheritdoc}
@ -36,7 +36,7 @@ class ContentTranslationEnableTest extends WebTestBase {
// No pending updates should be available.
$this->drupalGet('admin/reports/status');
$requirement_value = $this->cssSelect("details.system-status-report__entry summary:contains('Entity/field definitions') + div");
$this->assertEqual(t('Up to date'), trim((string) $requirement_value[0]));
$this->assertEqual(t('Up to date'), trim($requirement_value[0]->getText()));
$this->drupalGet('admin/config/regional/content-language');
// The node entity type should not be an option because it has no bundles.
@ -54,7 +54,7 @@ class ContentTranslationEnableTest extends WebTestBase {
// No pending updates should be available.
$this->drupalGet('admin/reports/status');
$requirement_value = $this->cssSelect("details.system-status-report__entry summary:contains('Entity/field definitions') + div");
$this->assertEqual(t('Up to date'), trim((string) $requirement_value[0]));
$this->assertEqual(t('Up to date'), trim($requirement_value[0]->getText()));
// Create a node type and check the content translation settings are now
// available for nodes.

View file

@ -1,9 +1,10 @@
<?php
namespace Drupal\content_translation\Tests;
namespace Drupal\Tests\content_translation\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Tests\NodeTestBase;
use Drupal\Tests\node\Functional\NodeTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests the content translation language that is set.
@ -12,6 +13,10 @@ use Drupal\node\Tests\NodeTestBase;
*/
class ContentTranslationLanguageChangeTest extends NodeTestBase {
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* Modules to enable.
*
@ -73,12 +78,12 @@ class ContentTranslationLanguageChangeTest extends NodeTestBase {
$edit = [
'title[0][value]' => 'english_title',
];
$this->drupalPostForm(NULL, $edit, t('Save and publish'));
$this->drupalPostForm(NULL, $edit, t('Save'));
// Create a translation in French.
$this->clickLink('Translate');
$this->clickLink('Add');
$this->drupalPostForm(NULL, [], t('Save and keep published (this translation)'));
$this->drupalPostForm(NULL, [], t('Save (this translation)'));
$this->clickLink('Translate');
// Edit English translation.
@ -90,7 +95,7 @@ class ContentTranslationLanguageChangeTest extends NodeTestBase {
'files[field_image_field_0]' => $images->uri,
];
$this->drupalPostForm(NULL, $edit, t('Upload'));
$this->drupalPostForm(NULL, ['field_image_field[0][alt]' => 'alternative_text'], t('Save and keep published (this translation)'));
$this->drupalPostForm(NULL, ['field_image_field[0][alt]' => 'alternative_text'], t('Save (this translation)'));
// Check that the translation languages are correct.
$node = $this->getNodeByTitle('english_title');
@ -109,13 +114,13 @@ class ContentTranslationLanguageChangeTest extends NodeTestBase {
'title[0][value]' => 'english_title',
'test_field_only_en_fr' => 'node created',
];
$this->drupalPostForm(NULL, $edit, t('Save and publish'));
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertEqual('node created', \Drupal::state()->get('test_field_only_en_fr'));
// Create a translation in French.
$this->clickLink('Translate');
$this->clickLink('Add');
$this->drupalPostForm(NULL, [], t('Save and keep published (this translation)'));
$this->drupalPostForm(NULL, [], t('Save (this translation)'));
$this->clickLink('Translate');
// Edit English translation.
@ -135,9 +140,9 @@ class ContentTranslationLanguageChangeTest extends NodeTestBase {
$this->assertRaw('<title>Edit Article english_title | Drupal</title>');
$edit = [
'langcode[0][value]' => 'en',
'field_image_field[0][alt]' => 'alternative_text'
'field_image_field[0][alt]' => 'alternative_text',
];
$this->drupalPostForm(NULL, $edit, t('Save and keep published (this translation)'));
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
// Check that the translation languages are correct.
$node = $this->getNodeByTitle('english_title');

View file

@ -98,14 +98,14 @@ class ContentTranslationOperationsTest extends NodeTestBase {
'access content' => TRUE,
]
);
$node->setPublished(FALSE)->save();
$node->setUnpublished()->save();
$this->drupalGet($node->urlInfo('drupal:content-translation-overview'));
$this->assertResponse(403);
$this->drupalLogout();
// Ensure the 'Translate' local task does not show up anymore when disabling
// translations for a content type.
$node->setPublished(TRUE)->save();
$node->setPublished()->save();
user_role_change_permissions(
Role::AUTHENTICATED_ID,
[
@ -136,7 +136,7 @@ class ContentTranslationOperationsTest extends NodeTestBase {
$this->assertFalse(content_translation_translate_access($node)->isAllowed());
$access_control_handler->resetCache();
$node->setPublished(TRUE);
$node->setPublished();
$node->save();
$this->assertTrue(content_translation_translate_access($node)->isAllowed());
$access_control_handler->resetCache();

View file

@ -0,0 +1,89 @@
<?php
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the "Flag as outdated" functionality with revision translations.
*
* @group content_translation
*/
class ContentTranslationOutdatedRevisionTranslationTest extends ContentTranslationPendingRevisionTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->enableContentModeration();
}
/**
* Tests that outdated revision translations work correctly.
*/
public function testFlagAsOutdatedHidden() {
// Create a test node.
$values = [
'title' => 'Test 1.1 EN',
'moderation_state' => 'published',
];
$id = $this->createEntity($values, 'en');
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->storage->load($id);
// Add a published Italian translation.
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
$entity->getEntityTypeId() => $id,
'source' => 'en',
'target' => 'it',
],
[
'language' => ConfigurableLanguage::load('it'),
'absolute' => FALSE,
]
);
$this->drupalGet($add_translation_url);
$this->assertFlagWidget();
$edit = [
'title[0][value]' => 'Test 1.2 IT',
'moderation_state[0][state]' => 'published',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
// Add a published French translation.
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
$entity->getEntityTypeId() => $id,
'source' => 'en',
'target' => 'fr',
],
[
'language' => ConfigurableLanguage::load('fr'),
'absolute' => FALSE,
]
);
$this->drupalGet($add_translation_url);
$this->assertFlagWidget();
$edit = [
'title[0][value]' => 'Test 1.3 FR',
'moderation_state[0][state]' => 'published',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
// Create an English draft.
$entity = $this->storage->loadUnchanged($id);
$en_edit_url = $this->getEditUrl($entity);
$this->drupalGet($en_edit_url);
$this->assertFlagWidget();
}
/**
* Checks whether the flag widget is displayed.
*/
protected function assertFlagWidget() {
$this->assertSession()->pageTextNotContains('Flag other translations as outdated');
$this->assertSession()->pageTextContains('Translations cannot be flagged as outdated when content is moderated.');
}
}

View file

@ -0,0 +1,179 @@
<?php
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Base class for pending revision translation tests.
*/
abstract class ContentTranslationPendingRevisionTestBase extends ContentTranslationTestBase {
use ContentTypeCreationTrait;
use ContentModerationTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['language', 'content_translation', 'content_moderation', 'node'];
/**
* The entity storage.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $storage;
/**
* Permissions common to all test accounts.
*
* @var string[]
*/
protected $commonPermissions;
/**
* The current test account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentAccount;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->entityTypeId = 'node';
$this->bundle = 'article';
$this->commonPermissions = [
'view any unpublished content',
"translate {$this->bundle} {$this->entityTypeId}",
"create content translations",
'use editorial transition create_new_draft',
'use editorial transition publish',
'use editorial transition archive',
'use editorial transition archived_draft',
'use editorial transition archived_published',
];
parent::setUp();
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $this->container->get('entity_type.manager');
$this->storage = $entity_type_manager->getStorage($this->entityTypeId);
// @todo Remove this line once https://www.drupal.org/node/2945928 is fixed.
$this->config('node.settings')->set('use_admin_theme', '1')->save();
}
/**
* Enables content moderation for the test entity type and bundle.
*/
protected function enableContentModeration() {
$this->drupalLogin($this->rootUser);
$workflow_id = 'editorial';
$this->drupalGet('/admin/config/workflow/workflows');
$edit['bundles[' . $this->bundle . ']'] = TRUE;
$this->drupalPostForm('admin/config/workflow/workflows/manage/' . $workflow_id . '/type/' . $this->entityTypeId, $edit, t('Save'));
// Ensure the parent environment is up-to-date.
// @see content_moderation_workflow_insert()
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
/** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
$router_builder = $this->container->get('router.builder');
$router_builder->rebuildIfNeeded();
}
/**
* {@inheritdoc}
*/
protected function getEditorPermissions() {
$editor_permissions = [
"edit any {$this->bundle} content",
"delete any {$this->bundle} content",
"view {$this->bundle} revisions",
"delete {$this->bundle} revisions",
];
return array_merge($editor_permissions, $this->commonPermissions);
}
/**
* {@inheritdoc}
*/
protected function getTranslatorPermissions() {
return array_merge(parent::getTranslatorPermissions(), $this->commonPermissions);
}
/**
* {@inheritdoc}
*/
protected function setupBundle() {
parent::setupBundle();
$this->createContentType(['type' => $this->bundle]);
$this->createEditorialWorkflow();
}
/**
* Loads the active revision translation for the specified entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being edited.
* @param string $langcode
* The translation language code.
*
* @return \Drupal\Core\Entity\ContentEntityInterface|null
* The active revision translation or NULL if none could be identified.
*/
protected function loadRevisionTranslation(ContentEntityInterface $entity, $langcode) {
// Explicitly invalidate the cache for that node, as the call below is
// statically cached.
$this->storage->resetCache([$entity->id()]);
$revision_id = $this->storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision = $revision_id ? $this->storage->loadRevision($revision_id) : NULL;
return $revision && $revision->hasTranslation($langcode) ? $revision->getTranslation($langcode) : NULL;
}
/**
* Returns the edit URL for the specified entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being edited.
*
* @return \Drupal\Core\Url
* The edit URL.
*/
protected function getEditUrl(ContentEntityInterface $entity) {
if ($entity->access('update', $this->loggedInUser)) {
$url = $entity->toUrl('edit-form');
}
else {
$url = $entity->toUrl('drupal:content-translation-edit');
$url->setRouteParameter('language', $entity->language()->getId());
}
return $url;
}
/**
* Returns the delete translation URL for the specified entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being edited.
*
* @return \Drupal\Core\Url
* The delete translation URL.
*/
protected function getDeleteUrl(ContentEntityInterface $entity) {
if ($entity->access('delete', $this->loggedInUser)) {
$url = $entity->toUrl('delete-form');
}
else {
$url = $entity->toUrl('drupal:content-translation-delete');
$url->setRouteParameter('language', $entity->language()->getId());
}
return $url;
}
}

View file

@ -0,0 +1,214 @@
<?php
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests that revision translation deletion is handled correctly.
*
* @group content_translation
*/
class ContentTranslationRevisionTranslationDeletionTest extends ContentTranslationPendingRevisionTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->enableContentModeration();
}
/**
* Tests that translation overview handles pending revisions correctly.
*/
public function testOverview() {
$index = 1;
$accounts = [
$this->rootUser,
$this->editor,
$this->translator,
];
foreach ($accounts as $account) {
$this->currentAccount = $account;
$this->doTestOverview($index++);
}
}
/**
* Performs a test run.
*
* @param int $index
* The test run index.
*/
public function doTestOverview($index) {
$this->drupalLogin($this->currentAccount);
// Create a test node.
$values = [
'title' => "Test $index.1 EN",
'moderation_state' => 'published',
];
$id = $this->createEntity($values, 'en');
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->storage->load($id);
// Add a draft translation and check that it is available only in the latest
// revision.
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
$entity->getEntityTypeId() => $id,
'source' => 'en',
'target' => 'it',
],
[
'language' => ConfigurableLanguage::load('it'),
'absolute' => FALSE,
]
);
$add_translation_href = $add_translation_url->toString();
$this->drupalGet($add_translation_url);
$edit = [
'title[0][value]' => "Test $index.2 IT",
'moderation_state[0][state]' => 'draft',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$entity = $this->storage->loadUnchanged($id);
$this->assertFalse($entity->hasTranslation('it'));
$it_revision = $this->loadRevisionTranslation($entity, 'it');
$this->assertTrue($it_revision->hasTranslation('it'));
// Check that translations cannot be deleted in drafts.
$overview_url = $entity->toUrl('drupal:content-translation-overview');
$this->drupalGet($overview_url);
$it_delete_url = $this->getDeleteUrl($it_revision);
$it_delete_href = $it_delete_url->toString();
$this->assertSession()->linkByHrefNotExists($it_delete_href);
$warning = 'The "Delete translation" action is only available for published translations.';
$this->assertSession()->pageTextContains($warning);
$this->drupalGet($this->getEditUrl($it_revision));
$this->assertSession()->buttonNotExists('Delete translation');
// Publish the translation and verify it can be deleted.
$edit = [
'title[0][value]' => "Test $index.3 IT",
'moderation_state[0][state]' => 'published',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$entity = $this->storage->loadUnchanged($id);
$this->assertTrue($entity->hasTranslation('it'));
$it_revision = $this->loadRevisionTranslation($entity, 'it');
$this->assertTrue($it_revision->hasTranslation('it'));
$this->drupalGet($overview_url);
$this->assertSession()->linkByHrefExists($it_delete_href);
$this->assertSession()->pageTextNotContains($warning);
$this->drupalGet($this->getEditUrl($it_revision));
$this->assertSession()->buttonExists('Delete translation');
// Create an English draft and verify the published translation was
// preserved.
$this->drupalLogin($this->editor);
$en_revision = $this->loadRevisionTranslation($entity, 'en');
$this->drupalGet($this->getEditUrl($en_revision));
$edit = [
'title[0][value]' => "Test $index.4 EN",
'moderation_state[0][state]' => 'draft',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$entity = $this->storage->loadUnchanged($id);
$this->assertTrue($entity->hasTranslation('it'));
$en_revision = $this->loadRevisionTranslation($entity, 'en');
$this->assertTrue($en_revision->hasTranslation('it'));
$this->drupalLogin($this->currentAccount);
// Delete the translation and verify that it is actually gone and that it is
// possible to create it again.
$this->drupalGet($it_delete_url);
$this->drupalPostForm(NULL, [], 'Delete Italian translation');
$entity = $this->storage->loadUnchanged($id);
$this->assertFalse($entity->hasTranslation('it'));
$it_revision = $this->loadRevisionTranslation($entity, 'it');
$this->assertTrue($it_revision->wasDefaultRevision());
$this->assertTrue($it_revision->hasTranslation('it'));
$this->assertTrue($it_revision->getRevisionId() < $entity->getRevisionId());
$this->drupalGet($overview_url);
$this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString());
$this->assertSession()->linkByHrefExists($add_translation_href);
// Publish the English draft and verify the translation is not accidentally
// restored.
$this->drupalLogin($this->editor);
$en_revision = $this->loadRevisionTranslation($entity, 'en');
$this->drupalGet($this->getEditUrl($en_revision));
$edit = [
'title[0][value]' => "Test $index.6 EN",
'moderation_state[0][state]' => 'published',
];
$this->drupalPostForm(NULL, $edit, t('Save'));
$entity = $this->storage->loadUnchanged($id);
$this->assertFalse($entity->hasTranslation('it'));
$this->drupalLogin($this->currentAccount);
// Create a published translation again and verify it could be deleted.
$this->drupalGet($add_translation_url);
$edit = [
'title[0][value]' => "Test $index.7 IT",
'moderation_state[0][state]' => 'published',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$entity = $this->storage->loadUnchanged($id);
$this->assertTrue($entity->hasTranslation('it'));
$it_revision = $this->loadRevisionTranslation($entity, 'it');
$this->assertTrue($it_revision->hasTranslation('it'));
$this->drupalGet($overview_url);
$this->assertSession()->linkByHrefExists($it_delete_href);
// Create a translation draft again and verify it cannot be deleted.
$this->drupalGet($this->getEditUrl($it_revision));
$edit = [
'title[0][value]' => "Test $index.8 IT",
'moderation_state[0][state]' => 'draft',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$entity = $this->storage->loadUnchanged($id);
$this->assertTrue($entity->hasTranslation('it'));
$it_revision = $this->loadRevisionTranslation($entity, 'it');
$this->assertTrue($it_revision->hasTranslation('it'));
$this->drupalGet($overview_url);
$this->assertSession()->linkByHrefNotExists($it_delete_href);
// Delete the translation draft and verify the translation can be deleted
// again, since the active revision is now a default revision.
$this->drupalLogin($this->editor);
$this->drupalGet($it_revision->toUrl('version-history'));
$revision_deletion_url = Url::fromRoute('node.revision_delete_confirm', [
'node' => $id,
'node_revision' => $it_revision->getRevisionId(),
],
[
'language' => ConfigurableLanguage::load('it'),
'absolute' => FALSE,
]
);
$revision_deletion_href = $revision_deletion_url->toString();
$this->getSession()->getDriver()->click("//a[@href='$revision_deletion_href']");
$this->drupalPostForm(NULL, [], 'Delete');
$this->drupalLogin($this->currentAccount);
$this->drupalGet($overview_url);
$this->assertSession()->linkByHrefExists($it_delete_href);
// Verify that now the translation can be deleted.
$this->drupalGet($it_delete_url);
$this->drupalPostForm(NULL, [], 'Delete Italian translation');
$entity = $this->storage->loadUnchanged($id);
$this->assertFalse($entity->hasTranslation('it'));
$it_revision = $this->loadRevisionTranslation($entity, 'it');
$this->assertTrue($it_revision->wasDefaultRevision());
$this->assertTrue($it_revision->hasTranslation('it'));
$this->assertTrue($it_revision->getRevisionId() < $entity->getRevisionId());
$this->drupalGet($overview_url);
$this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString());
$this->assertSession()->linkByHrefExists($add_translation_href);
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Drupal\content_translation\Tests;
namespace Drupal\Tests\content_translation\Functional;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\comment\Tests\CommentTestTrait;
@ -8,14 +8,14 @@ use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\Core\Language\Language;
use Drupal\field\Entity\FieldConfig;
use Drupal\language\Entity\ContentLanguageSettings;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the content translation settings UI.
*
* @group content_translation
*/
class ContentTranslationSettingsTest extends WebTestBase {
class ContentTranslationSettingsTest extends BrowserTestBase {
use CommentTestTrait;
@ -154,7 +154,7 @@ class ContentTranslationSettingsTest extends WebTestBase {
'settings[node][article][settings][language][langcode]' => 'current_interface',
'settings[node][article][settings][language][language_alterable]' => TRUE,
'settings[node][article][translatable]' => TRUE,
'settings[node][article][fields][title]' => TRUE
'settings[node][article][fields][title]' => TRUE,
];
$this->assertSettings('node', NULL, TRUE, $edit);
@ -193,7 +193,7 @@ class ContentTranslationSettingsTest extends WebTestBase {
$elements = $this->xpath('//select[@id="edit-settings-node-article-settings-language-langcode"]/option');
// Compare values inside the option elements with expected values.
for ($i = 0; $i < count($elements); $i++) {
$this->assertEqual($elements[$i]->attributes()->{'value'}, $expected_elements[$i]);
$this->assertEqual($elements[$i]->getValue(), $expected_elements[$i]);
}
}

View file

@ -1,11 +1,12 @@
<?php
namespace Drupal\content_translation\Tests;
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests the field synchronization behavior for the image field.
@ -14,6 +15,10 @@ use Drupal\file\Entity\File;
*/
class ContentTranslationSyncImageTest extends ContentTranslationTestBase {
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* The cardinality of the image field.
*

View file

@ -2,6 +2,7 @@
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\field\Entity\FieldConfig;
use Drupal\language\Entity\ConfigurableLanguage;
@ -138,7 +139,7 @@ abstract class ContentTranslationTestBase extends BrowserTestBase {
* Returns an array of permissions needed for the administrator.
*/
protected function getAdministratorPermissions() {
return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), ['administer content translation']);
return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), ['administer languages', 'administer content translation']);
}
/**
@ -236,4 +237,24 @@ abstract class ContentTranslationTestBase extends BrowserTestBase {
return $entity->id();
}
/**
* Returns the edit URL for the specified entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being edited.
*
* @return \Drupal\Core\Url
* The edit URL.
*/
protected function getEditUrl(ContentEntityInterface $entity) {
if ($entity->access('update', $this->loggedInUser)) {
$url = $entity->toUrl('edit-form');
}
else {
$url = $entity->toUrl('drupal:content-translation-edit');
$url->setRouteParameter('language', $entity->language()->getId());
}
return $url;
}
}

View file

@ -25,7 +25,7 @@ class ContentTranslationUISkipTest extends BrowserTestBase {
$admin_user = $this->drupalCreateUser([
'translate any entity',
'administer content translation',
'administer languages'
'administer languages',
]);
$this->drupalLogin($admin_user);
// Visit the content translation.

View file

@ -10,8 +10,7 @@ use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests the Content Translation UI.
@ -35,8 +34,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
protected $testLanguageSelector = TRUE;
/**
* Flag that tells whether the HTML escaping of all languages works or not
* after SafeMarkup change.
* Flag to determine if "all languages" rendering is tested.
*
* @var bool
*/
@ -111,12 +109,11 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
'target' => $langcode,
], ['language' => $language]);
$this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), $this->getFormSubmitActionForNewTranslation($entity, $langcode));
// Assert that HTML is escaped in "all languages" in UI after SafeMarkup
// change.
// Assert that HTML is not escaped unexpectedly.
if ($this->testHTMLEscapeForAllLanguages) {
$this->assertNoRaw('&lt;span class=&quot;translation-entity-all-languages&quot;&gt;(all languages)&lt;/span&gt;');
$this->assertRaw('<span class="translation-entity-all-languages">(all languages)</span>');
@ -139,10 +136,10 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$author_field_name = $entity->hasField('content_translation_uid') ? 'content_translation_uid' : 'uid';
if ($entity->getFieldDefinition($author_field_name)->isTranslatable()) {
$this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(),
SafeMarkup::format('Author of the target translation @langcode correctly stored for translatable owner field.', ['@langcode' => $langcode]));
new FormattableMarkup('Author of the target translation @langcode correctly stored for translatable owner field.', ['@langcode' => $langcode]));
$this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(),
SafeMarkup::format('Author of the target translation @target different from the author of the source translation @source for translatable owner field.',
new FormattableMarkup('Author of the target translation @target different from the author of the source translation @source for translatable owner field.',
['@target' => $langcode, '@source' => $default_langcode]));
}
else {
@ -152,7 +149,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$created_field_name = $entity->hasField('content_translation_created') ? 'content_translation_created' : 'created';
if ($entity->getFieldDefinition($created_field_name)->isTranslatable()) {
$this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(),
SafeMarkup::format('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.',
new FormattableMarkup('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.',
['@target' => $langcode, '@source' => $default_langcode]));
}
else {
@ -176,7 +173,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode
'target' => $langcode,
], ['language' => $language]);
// This does not save anything, it merely reloads the form and fills in the
// fields with the values from the different source language.
@ -190,7 +187,7 @@ abstract class ContentTranslationUITestBase extends ContentTranslationTestBase {
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $source_langcode,
'target' => $langcode
'target' => $langcode,
], ['language' => $language]);
$this->drupalPostForm($add_url, $edit, $this->getFormSubmitActionForNewTranslation($entity, $langcode));
$storage->resetCache([$this->entityId]);

View file

@ -0,0 +1,177 @@
<?php
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the untranslatable fields behaviors.
*
* @group content_translation
*/
class ContentTranslationUntranslatableFieldsTest extends ContentTranslationPendingRevisionTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['field_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Configure one field as untranslatable.
$this->drupalLogin($this->administrator);
$edit = [
'settings[' . $this->entityTypeId . '][' . $this->bundle . '][fields][' . $this->fieldName . ']' => 0,
];
$this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
$entity_field_manager = $this->container->get('entity_field.manager');
$entity_field_manager->clearCachedFieldDefinitions();
$definitions = $entity_field_manager->getFieldDefinitions($this->entityTypeId, $this->bundle);
$this->assertFalse($definitions[$this->fieldName]->isTranslatable());
}
/**
* {@inheritdoc}
*/
protected function setupTestFields() {
parent::setupTestFields();
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_multilingual',
'type' => 'test_field',
'entity_type' => $this->entityTypeId,
'cardinality' => 1,
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $this->bundle,
'label' => 'Untranslatable-but-visible test field',
'translatable' => FALSE,
])->save();
entity_get_form_display($this->entityTypeId, $this->bundle, 'default')
->setComponent('field_multilingual', [
'type' => 'test_field_widget_multilingual',
])
->save();
}
/**
* Tests that hiding untranslatable field widgets works correctly.
*/
public function testHiddenWidgets() {
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $this->container->get('entity_type.manager');
$id = $this->createEntity(['title' => $this->randomString()], 'en');
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $entity_type_manager
->getStorage($this->entityTypeId)
->load($id);
// Check that the untranslatable field widget is displayed on the edit form
// and no translatability clue is displayed yet.
$en_edit_url = $entity->toUrl('edit-form');
$this->drupalGet($en_edit_url);
$field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
$this->assertNotEmpty($this->xpath($field_xpath));
$clue_xpath = '//label[@for="edit-' . strtr($this->fieldName, '_', '-') . '-0-value"]/span[text()="(all languages)"]';
$this->assertEmpty($this->xpath($clue_xpath));
$this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
// Add a translation and check that the untranslatable field widget is
// displayed on the translation and edit forms along with translatability
// clues.
$add_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => 'en',
'target' => 'it',
]);
$this->drupalGet($add_url);
$this->assertNotEmpty($this->xpath($field_xpath));
$this->assertNotEmpty($this->xpath($clue_xpath));
$this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
$this->drupalPostForm(NULL, [], 'Save');
// Check that the widget is displayed along with its clue in the edit form
// for both languages.
$this->drupalGet($en_edit_url);
$this->assertNotEmpty($this->xpath($field_xpath));
$this->assertNotEmpty($this->xpath($clue_xpath));
$it_edit_url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load('it')]);
$this->drupalGet($it_edit_url);
$this->assertNotEmpty($this->xpath($field_xpath));
$this->assertNotEmpty($this->xpath($clue_xpath));
// Configure untranslatable field widgets to be hidden on non-default
// language edit forms.
$settings_key = 'settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][content_translation][untranslatable_fields_hide]';
$settings_url = 'admin/config/regional/content-language';
$this->drupalPostForm($settings_url, [$settings_key => 1], 'Save configuration');
// Verify that the widget is displayed in the default language edit form,
// but no clue is displayed.
$this->drupalGet($en_edit_url);
$field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
$this->assertNotEmpty($this->xpath($field_xpath));
$this->assertEmpty($this->xpath($clue_xpath));
$this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
// Verify no widget is displayed on the non-default language edit form.
$this->drupalGet($it_edit_url);
$this->assertEmpty($this->xpath($field_xpath));
$this->assertEmpty($this->xpath($clue_xpath));
$this->assertSession()->pageTextContains('Untranslatable-but-visible test field');
// Verify a warning is displayed.
$this->assertSession()->pageTextContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
$edit_path = $entity->toUrl('edit-form')->toString();
$link_xpath = '//a[@href=:edit_path and text()="Edit them on the original language form"]';
$elements = $this->xpath($link_xpath, [':edit_path' => $edit_path]);
$this->assertNotEmpty($elements);
// Configure untranslatable field widgets to be displayed on non-default
// language edit forms.
$this->drupalPostForm($settings_url, [$settings_key => 0], 'Save configuration');
// Check that the widget is displayed along with its clue in the edit form
// for both languages.
$this->drupalGet($en_edit_url);
$this->assertNotEmpty($this->xpath($field_xpath));
$this->assertNotEmpty($this->xpath($clue_xpath));
$this->drupalGet($it_edit_url);
$this->assertNotEmpty($this->xpath($field_xpath));
$this->assertNotEmpty($this->xpath($clue_xpath));
// Enable content moderation and verify that widgets are hidden despite them
// being configured to be displayed.
$this->enableContentModeration();
$this->drupalGet($it_edit_url);
$this->assertEmpty($this->xpath($field_xpath));
$this->assertEmpty($this->xpath($clue_xpath));
// Verify a warning is displayed.
$this->assertSession()->pageTextContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
$elements = $this->xpath($link_xpath, [':edit_path' => $edit_path]);
$this->assertNotEmpty($elements);
// Verify that checkboxes on the language content settings page are checked
// and disabled for moderated bundles.
$this->drupalGet($settings_url);
$input_xpath = '//input[@name="settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][content_translation][untranslatable_fields_hide]" and @value=1 and @disabled="disabled"]';
$elements = $this->xpath($input_xpath);
$this->assertNotEmpty($elements);
$this->drupalPostForm(NULL, [$settings_key => 0], 'Save configuration');
$elements = $this->xpath($input_xpath);
$this->assertNotEmpty($elements);
}
}

View file

@ -1,10 +1,11 @@
<?php
namespace Drupal\content_translation\Tests;
namespace Drupal\Tests\content_translation\Functional;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\user\UserInterface;
/**
@ -14,6 +15,8 @@ use Drupal\user\UserInterface;
*/
class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* The entity used for testing.
*
@ -158,19 +161,19 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
// Check whether the user is allowed to access the entity form in edit mode.
$edit_url = $this->entity->urlInfo('edit-form', $options);
$this->drupalGet($edit_url, $options);
$this->assertResponse($expected_status['edit'], SafeMarkup::format('The @user_label has the expected edit access.', $args));
$this->assertResponse($expected_status['edit'], new FormattableMarkup('The @user_label has the expected edit access.', $args));
// Check whether the user is allowed to access the entity delete form.
$delete_url = $this->entity->urlInfo('delete-form', $options);
$this->drupalGet($delete_url, $options);
$this->assertResponse($expected_status['delete'], SafeMarkup::format('The @user_label has the expected delete access.', $args));
$this->assertResponse($expected_status['delete'], new FormattableMarkup('The @user_label has the expected delete access.', $args));
// Check whether the user is allowed to access the translation overview.
$langcode = $this->langcodes[1];
$options['language'] = $languages[$langcode];
$translations_url = $this->entity->url('drupal:content-translation-overview', $options);
$this->drupalGet($translations_url);
$this->assertResponse($expected_status['overview'], SafeMarkup::format('The @user_label has the expected translation overview access.', $args));
$this->assertResponse($expected_status['overview'], new FormattableMarkup('The @user_label has the expected translation overview access.', $args));
// Check whether the user is allowed to create a translation.
$add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [$this->entityTypeId => $this->entity->id(), 'source' => $default_langcode, 'target' => $langcode], $options);
@ -186,7 +189,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
else {
$this->drupalGet($add_translation_url);
}
$this->assertResponse($expected_status['add_translation'], SafeMarkup::format('The @user_label has the expected translation creation access.', $args));
$this->assertResponse($expected_status['add_translation'], new FormattableMarkup('The @user_label has the expected translation creation access.', $args));
// Check whether the user is allowed to edit a translation.
$langcode = $this->langcodes[2];
@ -214,7 +217,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
else {
$this->drupalGet($edit_translation_url);
}
$this->assertResponse($expected_status['edit_translation'], SafeMarkup::format('The @user_label has the expected translation edit access.', $args));
$this->assertResponse($expected_status['edit_translation'], new FormattableMarkup('The @user_label has the expected translation edit access.', $args));
// Check whether the user is allowed to delete a translation.
$langcode = $this->langcodes[2];
@ -242,7 +245,7 @@ class ContentTranslationWorkflowsTest extends ContentTranslationTestBase {
else {
$this->drupalGet($delete_translation_url);
}
$this->assertResponse($expected_status['delete_translation'], SafeMarkup::format('The @user_label has the expected translation deletion access.', $args));
$this->assertResponse($expected_status['delete_translation'], new FormattableMarkup('The @user_label has the expected translation deletion access.', $args));
}
/**

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\Tests\content_translation\Functional\Update;
use Drupal\Core\Language\LanguageInterface;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
/**
* Tests the upgrade path for the Content Translation module.
*
* @group Update
* @group legacy
*/
class ContentTranslationUpdateTest extends UpdatePathTestBase {
use EntityDefinitionTestTrait;
/**
* The database connection used.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The entity definition update manager.
*
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
*/
protected $entityDefinitionUpdateManager;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->database = \Drupal::database();
$this->entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager();
$this->entityManager = \Drupal::entityManager();
$this->state = \Drupal::state();
}
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul.php.gz',
];
}
/**
* Tests that initial values for metadata fields are populated correctly.
*/
public function testContentTranslationUpdate8400() {
$this->updateEntityTypeToTranslatable();
// The test database dump contains NULL values for
// 'content_translation_source', 'content_translation_outdated' and
// 'content_translation_status' for the first 50 test entities.
// @see _entity_test_update_create_test_entities()
$first_entity_record = $this->database->select('entity_test_update_data', 'etud')
->fields('etud')
->condition('etud.id', 1)
->execute()
->fetchAllAssoc('id');
$this->assertNull($first_entity_record[1]->content_translation_source);
$this->assertNull($first_entity_record[1]->content_translation_outdated);
$this->assertNull($first_entity_record[1]->content_translation_status);
$this->runUpdates();
// After running the updates, all those fields should be populated with
// their default values.
$first_entity_record = $this->database->select('entity_test_update_data', 'etud')
->fields('etud')
->condition('etud.id', 1)
->execute()
->fetchAllAssoc('id');
$this->assertEqual(LanguageInterface::LANGCODE_NOT_SPECIFIED, $first_entity_record[1]->content_translation_source);
$this->assertEqual(0, $first_entity_record[1]->content_translation_outdated);
$this->assertEqual(1, $first_entity_record[1]->content_translation_status);
}
}

View file

@ -1,8 +1,8 @@
<?php
namespace Drupal\content_translation\Tests\Views;
namespace Drupal\Tests\content_translation\Functional\Views;
use Drupal\views_ui\Tests\UITestBase;
use Drupal\Tests\views_ui\Functional\UITestBase;
/**
* Tests the views UI when content_translation is enabled.

View file

@ -1,8 +1,8 @@
<?php
namespace Drupal\content_translation\Tests\Views;
namespace Drupal\Tests\content_translation\Functional\Views;
use Drupal\content_translation\Tests\ContentTranslationTestBase;
use Drupal\Tests\content_translation\Functional\ContentTranslationTestBase;
use Drupal\views\Tests\ViewTestData;
use Drupal\Core\Language\Language;
use Drupal\user\Entity\User;

View file

@ -0,0 +1,72 @@
<?php
namespace Drupal\Tests\content_translation\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests that contextual links are available for content translation.
*
* @group content_translation
*/
class ContentTranslationContextualLinksTest extends WebDriverTestBase {
/**
* The 'translator' user to use during testing.
*
* @var \Drupal\user\UserInterface
*/
protected $translator;
/**
* {@inheritdoc}
*/
public static $modules = ['content_translation', 'contextual', 'node'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Set up an additional language.
ConfigurableLanguage::createFromLangcode('es')->save();
// Create a content type.
$this->drupalCreateContentType(['type' => 'page']);
// Enable content translation.
$this->drupalLogin($this->rootUser);
$this->drupalGet('admin/config/regional/content-language');
$edit = [
'entity_types[node]' => TRUE,
'settings[node][page][translatable]' => TRUE,
];
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->drupalLogout();
// Create a translator user.
$permissions = [
'access contextual links',
'administer nodes',
'edit any page content',
'translate any entity',
];
$this->translator = $this->drupalCreateUser($permissions);
}
/**
* Tests that a contextual link is available for translating a node.
*/
public function testContentTranslationContextualLinks() {
$node = $this->drupalCreateNode(['type' => 'page', 'title' => 'Test']);
// Check that the translate link appears on the node page.
$this->drupalLogin($this->translator);
$this->drupalGet('node/' . $node->id());
$link = $this->assertSession()->waitForElement('css', '[data-contextual-id^="node:node=1"] .contextual-links a:contains("Translate")');
$this->assertContains('node/1/translations', $link->getAttribute('href'));
}
}

View file

@ -33,6 +33,7 @@ class ContentTranslationConfigImportTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->installConfig(['system']);
$this->installEntitySchema('entity_test_mul');
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
@ -74,7 +75,7 @@ class ContentTranslationConfigImportTest extends KernelTestBase {
'langcode' => 'en',
'status' => TRUE,
'dependencies' => [
'module' => ['content_translation']
'module' => ['content_translation'],
],
'id' => $config_id,
'target_entity_type_id' => 'entity_test_mul',

View file

@ -0,0 +1,90 @@
<?php
namespace Drupal\Tests\content_translation\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\entity_test\Entity\EntityTestMul;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the Content Translation bundle info logic.
*
* @group content_translation
*/
class ContentTranslationEntityBundleInfoTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['user', 'language', 'content_translation_test', 'content_translation', 'entity_test'];
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslationManager;
/**
* The bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfo
*/
protected $bundleInfo;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->contentTranslationManager = $this->container->get('content_translation.manager');
$this->bundleInfo = $this->container->get('entity_type.bundle.info');
$this->installEntitySchema('entity_test_mul');
ConfigurableLanguage::createFromLangcode('it')->save();
}
/**
* Tests that modules can know whether bundles are translatable.
*/
public function testHookInvocationOrder() {
$this->contentTranslationManager->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
$this->bundleInfo->clearCachedBundles();
$this->bundleInfo->getAllBundleInfo();
// Verify that the test module comes first in the module list, which would
// normally make its hook implementation to be invoked first.
/** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
$module_handler = $this->container->get('module_handler');
$module_list = $module_handler->getModuleList();
$expected_modules = [
'content_translation_test',
'content_translation',
];
$actual_modules = array_keys(array_intersect_key($module_list, array_flip($expected_modules)));
$this->assertEquals($expected_modules, $actual_modules);
// Check that the "content_translation_test" hook implementation has access
// to the "translatable" bundle info property.
/** @var \Drupal\Core\State\StateInterface $state */
$state = $this->container->get('state');
$this->assertTrue($state->get('content_translation_test.translatable'));
}
/**
* Tests that field synchronization is skipped for disabled bundles.
*/
public function testFieldSynchronizationWithDisabledBundle() {
$entity = EntityTestMul::create();
$entity->save();
/** @var \Drupal\Core\Entity\ContentEntityInterface $translation */
$translation = $entity->addTranslation('it');
$translation->save();
$this->assertTrue($entity->isTranslatable());
}
}

View file

@ -0,0 +1,482 @@
<?php
namespace Drupal\Tests\content_translation\Kernel;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\Entity\User;
/**
* Tests the field synchronization logic when revisions are involved.
*
* @group content_translation
*/
class ContentTranslationFieldSyncRevisionTest extends EntityKernelTestBase {
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['file', 'image', 'language', 'content_translation', 'simpletest', 'content_translation_test'];
/**
* The synchronized field name.
*
* @var string
*/
protected $fieldName = 'sync_field';
/**
* The content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface|\Drupal\content_translation\BundleTranslationSettingsInterface
*/
protected $contentTranslationManager;
/**
* The test entity storage.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $storage;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$entity_type_id = 'entity_test_mulrev';
$this->installEntitySchema($entity_type_id);
$this->installEntitySchema('file');
$this->installSchema('file', ['file_usage']);
ConfigurableLanguage::createFromLangcode('it')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
/** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
$field_storage_config = FieldStorageConfig::create([
'field_name' => $this->fieldName,
'type' => 'image',
'entity_type' => $entity_type_id,
'cardinality' => 1,
'translatable' => 1,
]);
$field_storage_config->save();
$field_config = FieldConfig::create([
'entity_type' => $entity_type_id,
'field_name' => $this->fieldName,
'bundle' => $entity_type_id,
'label' => 'Synchronized field',
'translatable' => 1,
]);
$field_config->save();
$property_settings = [
'alt' => 'alt',
'title' => 'title',
'file' => 0,
];
$field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings);
$field_config->save();
$this->entityManager->clearCachedDefinitions();
$this->contentTranslationManager = $this->container->get('content_translation.manager');
$this->contentTranslationManager->setEnabled($entity_type_id, $entity_type_id, TRUE);
$this->storage = $this->entityManager->getStorage($entity_type_id);
foreach ($this->getTestFiles('image') as $file) {
$entity = File::create((array) $file + ['status' => 1]);
$entity->save();
}
$this->state->set('content_translation.entity_access.file', ['view' => TRUE]);
$account = User::create([
'name' => $this->randomMachineName(),
'status' => 1,
]);
$account->save();
}
/**
* Checks that field synchronization works as expected with revisions.
*
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::create
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::validate
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::hasSynchronizedPropertyChanges
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::getFieldSynchronizedProperties
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeFields
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeItems
*/
public function testFieldSynchronizationAndValidation() {
// Test that when untranslatable field widgets are displayed, synchronized
// field properties can be changed only in default revisions.
$this->setUntranslatableFieldWidgetsDisplay(TRUE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [1, 1, 1, 'Alt 1 EN']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
$en_revision = $this->createRevision($entity, FALSE);
$en_revision->get($this->fieldName)->target_id = 2;
$violations = $en_revision->validate();
$this->assertViolations($violations);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation, FALSE);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->target_id = 2;
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
$violations = $it_revision->validate();
$this->assertViolations($violations);
$it_revision->isDefaultRevision(TRUE);
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [2, 2, 2, 'Alt 1 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($en_revision, FALSE);
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [3, 2, 2, 'Alt 3 EN', 'Alt 2 IT']);
$it_revision = $this->createRevision($it_revision, FALSE);
$it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [4, 2, 2, 'Alt 1 EN', 'Alt 4 IT']);
$en_revision = $this->createRevision($en_revision);
$en_revision->get($this->fieldName)->alt = 'Alt 5 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [5, 2, 2, 'Alt 5 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($en_revision);
$en_revision->get($this->fieldName)->target_id = 6;
$en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [6, 6, 6, 'Alt 6 EN', 'Alt 2 IT']);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [7, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
// Test that when untranslatable field widgets are hidden, synchronized
// field properties can be changed only when editing the default
// translation. This may lead to temporarily desynchronized values, when
// saving a pending revision for the default translation that changes a
// synchronized property (see revision 11).
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [8, 1, 1, 'Alt 1 EN']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
$en_revision = $this->createRevision($entity, FALSE);
$en_revision->get($this->fieldName)->target_id = 2;
$en_revision->get($this->fieldName)->alt = 'Alt 2 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [9, 2, 2, 'Alt 2 EN']);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation, FALSE);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->target_id = 3;
$violations = $it_revision->validate();
$this->assertViolations($violations);
$it_revision->isDefaultRevision(TRUE);
$violations = $it_revision->validate();
$this->assertViolations($violations);
$it_revision = $this->createRevision($it_translation);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->alt = 'Alt 3 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [10, 1, 1, 'Alt 1 EN', 'Alt 3 IT']);
$en_revision = $this->createRevision($en_revision, FALSE);
$en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [11, 2, 1, 'Alt 4 EN', 'Alt 3 IT']);
$it_revision = $this->createRevision($it_revision, FALSE);
$it_revision->get($this->fieldName)->alt = 'Alt 5 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [12, 1, 1, 'Alt 1 EN', 'Alt 5 IT']);
$en_revision = $this->createRevision($en_revision);
$en_revision->get($this->fieldName)->target_id = 6;
$en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [13, 6, 6, 'Alt 6 EN', 'Alt 3 IT']);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->target_id = 7;
$violations = $it_revision->validate();
$this->assertViolations($violations);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [14, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
// Test that creating a default revision starting from a pending revision
// having changes to synchronized properties, without introducing new
// changes works properly.
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [15, 1, 1, 'Alt 1 EN']);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [16, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
$en_revision = $this->createRevision($entity);
$en_revision->get($this->fieldName)->target_id = 3;
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [17, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($entity, FALSE);
$en_revision->get($this->fieldName)->target_id = 4;
$en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [18, 4, 3, 'Alt 4 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($entity);
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [19, 4, 4, 'Alt 4 EN', 'Alt 2 IT']);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->alt = 'Alt 6 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [20, 4, 4, 'Alt 4 EN', 'Alt 6 IT']);
// Check that we are not allowed to perform changes to multiple translations
// in pending revisions when synchronized properties are involved.
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [21, 1, 1, 'Alt 1 EN']);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [22, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
$en_revision = $this->createRevision($entity, FALSE);
$en_revision->get($this->fieldName)->target_id = 2;
$en_revision->getTranslation('it')->get($this->fieldName)->alt = 'Alt 3 IT';
$violations = $en_revision->validate();
$this->assertViolations($violations);
// Test that when saving a new default revision starting from a pending
// revision, outdated synchronized properties do not override more recent
// ones.
$this->setUntranslatableFieldWidgetsDisplay(TRUE);
$entity = $this->saveNewEntity();
$entity_id = $entity->id();
$this->assertLatestRevisionFieldValues($entity_id, [23, 1, 1, 'Alt 1 EN']);
$it_translation = $entity->addTranslation('it', $entity->toArray());
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
$it_revision = $this->createRevision($it_translation, FALSE);
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
$metadata->setSource('en');
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [24, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
$en_revision = $this->createRevision($entity);
$en_revision->get($this->fieldName)->target_id = 3;
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
$violations = $en_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($en_revision);
$this->assertLatestRevisionFieldValues($entity_id, [25, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
$it_revision = $this->createRevision($it_revision);
$it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
$violations = $it_revision->validate();
$this->assertEmpty($violations);
$this->storage->save($it_revision);
$this->assertLatestRevisionFieldValues($entity_id, [26, 3, 3, 'Alt 3 EN', 'Alt 4 IT']);
}
/**
* Sets untranslatable field widgets' display status.
*
* @param bool $display
* Whether untranslatable field widgets should be displayed.
*/
protected function setUntranslatableFieldWidgetsDisplay($display) {
$entity_type_id = $this->storage->getEntityTypeId();
$settings = ['untranslatable_fields_hide' => !$display];
$this->contentTranslationManager->setBundleTranslationSettings($entity_type_id, $entity_type_id, $settings);
/** @var \Drupal\Core\Entity\EntityTypeBundleInfo $bundle_info */
$bundle_info = $this->container->get('entity_type.bundle.info');
$bundle_info->clearCachedBundles();
}
/**
* @return \Drupal\Core\Entity\ContentEntityInterface
*/
protected function saveNewEntity() {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = EntityTestMulRev::create([
'uid' => 1,
'langcode' => 'en',
$this->fieldName => [
'target_id' => 1,
'alt' => 'Alt 1 EN',
],
]);
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
$metadata->setSource(LanguageInterface::LANGCODE_NOT_SPECIFIED);
$violations = $entity->validate();
$this->assertEmpty($violations);
$this->storage->save($entity);
return $entity;
}
/**
* Creates a new revision starting from the latest translation-affecting one.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $translation
* The translation to be revisioned.
* @param bool $default
* (optional) Whether the new revision should be marked as default. Defaults
* to TRUE.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* An entity revision object.
*/
protected function createRevision(ContentEntityInterface $translation, $default = TRUE) {
if (!$translation->isNewTranslation()) {
$langcode = $translation->language()->getId();
$revision_id = $this->storage->getLatestTranslationAffectedRevisionId($translation->id(), $langcode);
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision = $this->storage->loadRevision($revision_id);
$translation = $revision->getTranslation($langcode);
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision = $this->storage->createRevision($translation, $default);
return $revision;
}
/**
* Asserts that the expected violations were found.
*
* @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
* A list of violations.
*/
protected function assertViolations(EntityConstraintViolationListInterface $violations) {
$entity_type_id = $this->storage->getEntityTypeId();
$settings = $this->contentTranslationManager->getBundleTranslationSettings($entity_type_id, $entity_type_id);
$message = !empty($settings['untranslatable_fields_hide']) ?
'Non-translatable field elements can only be changed when updating the original language.' :
'Non-translatable field elements can only be changed when updating the current revision.';
$list = [];
foreach ($violations as $violation) {
if ((string) $violation->getMessage() === $message) {
$list[] = $violation;
}
}
$this->assertCount(1, $list);
}
/**
* Asserts that the latest revision has the expected field values.
*
* @param $entity_id
* The entity ID.
* @param array $expected_values
* An array of expected values in the following order:
* - revision ID
* - target ID (en)
* - target ID (it)
* - alt (en)
* - alt (it)
*/
protected function assertLatestRevisionFieldValues($entity_id, array $expected_values) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->storage->loadRevision($this->storage->getLatestRevisionId($entity_id));
@list($revision_id, $target_id_en, $target_id_it, $alt_en, $alt_it) = $expected_values;
$this->assertEquals($revision_id, $entity->getRevisionId());
$this->assertEquals($target_id_en, $entity->get($this->fieldName)->target_id);
$this->assertEquals($alt_en, $entity->get($this->fieldName)->alt);
if ($entity->hasTranslation('it')) {
$it_translation = $entity->getTranslation('it');
$this->assertEquals($target_id_it, $it_translation->get($this->fieldName)->target_id);
$this->assertEquals($alt_it, $it_translation->get($this->fieldName)->alt);
}
}
}

View file

@ -59,7 +59,7 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager'));
$this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager'), $this->container->get('plugin.manager.field.field_type'));
$this->synchronized = ['sync1', 'sync2'];
$this->columns = array_merge($this->synchronized, ['var1', 'var2']);
$this->langcodes = ['en', 'it', 'fr', 'de', 'es'];
@ -181,13 +181,21 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
// their delta.
$delta_callbacks = [
// Continuous field values: all values are equal.
function($delta) { return TRUE; },
function ($delta) {
return TRUE;
},
// Alternated field values: only the even ones are equal.
function($delta) { return $delta % 2 !== 0; },
function ($delta) {
return $delta % 2 !== 0;
},
// Sparse field values: only the "middle" ones are equal.
function($delta) { return $delta === 1 || $delta === 2; },
function ($delta) {
return $delta === 1 || $delta === 2;
},
// Sparse field values: only the "extreme" ones are equal.
function($delta) { return $delta === 0 || $delta === 3; },
function ($delta) {
return $delta === 0 || $delta === 3;
},
];
foreach ($delta_callbacks as $delta_callback) {
@ -241,7 +249,7 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
for ($delta = 0; $delta < $this->cardinality; $delta++) {
foreach ($this->columns as $column) {
// If the column is synchronized, the value should have been synced,
// for unsychronized columns, the value must not change.
// for unsynchronized columns, the value must not change.
$expected_value = in_array($column, $this->synchronized) ? $changed_items[$delta][$column] : $this->unchangedFieldValues[$langcode][$delta][$column];
$this->assertEqual($field_values[$langcode][$delta][$column], $expected_value, "Differing Item $delta column $column for langcode $langcode synced correctly");
}

View file

@ -0,0 +1,127 @@
<?php
namespace Drupal\Tests\content_translation\Kernel\Migrate\d6;
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\taxonomy\TermInterface;
/**
* Test migration of translated taxonomy terms.
*
* @group migrate_drupal_6
*/
class MigrateTaxonomyTermTranslationTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'content_translation',
'language',
'menu_ui',
// Required for translation migrations.
'migrate_drupal_multilingual',
'node',
'taxonomy',
];
/**
* The cached taxonomy tree items, keyed by vid and tid.
*
* @var array
*/
protected $treeData = [];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('taxonomy_term');
$this->installConfig(static::$modules);
$this->executeMigrations([
'd6_node_type',
'd6_field',
'd6_taxonomy_vocabulary',
'd6_field_instance',
'd6_taxonomy_term',
'd6_taxonomy_term_translation',
]);
}
/**
* Validate a migrated term contains the expected values.
*
* @param int $id
* Entity ID to load and check.
* @param string $expected_language
* The language code for this term.
* @param string $expected_label
* The label the migrated entity should have.
* @param string $expected_vid
* The parent vocabulary the migrated entity should have.
* @param string $expected_description
* The description the migrated entity should have.
* @param string $expected_format
* The format the migrated entity should have.
* @param int $expected_weight
* The weight the migrated entity should have.
* @param array $expected_parents
* The parent terms the migrated entity should have.
* @param int $expected_field_integer_value
* The value the migrated entity field should have.
* @param int $expected_term_reference_tid
* The term reference ID the migrated entity field should have.
*/
protected function assertEntity($id, $expected_language, $expected_label, $expected_vid, $expected_description = '', $expected_format = NULL, $expected_weight = 0, $expected_parents = [], $expected_field_integer_value = NULL, $expected_term_reference_tid = NULL) {
/** @var \Drupal\taxonomy\TermInterface $entity */
$entity = Term::load($id);
$this->assertInstanceOf(TermInterface::class, $entity);
$this->assertSame($expected_language, $entity->language()->getId());
$this->assertSame($expected_label, $entity->label());
$this->assertSame($expected_vid, $entity->bundle());
$this->assertSame($expected_description, $entity->getDescription());
$this->assertSame($expected_format, $entity->getFormat());
$this->assertSame($expected_weight, $entity->getWeight());
$this->assertHierarchy($expected_vid, $id, $expected_parents);
}
/**
* Assert that a term is present in the tree storage, with the right parents.
*
* @param string $vid
* Vocabulary ID.
* @param int $tid
* ID of the term to check.
* @param array $parent_ids
* The expected parent term IDs.
*/
protected function assertHierarchy($vid, $tid, array $parent_ids) {
if (!isset($this->treeData[$vid])) {
$tree = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($vid);
$this->treeData[$vid] = [];
foreach ($tree as $item) {
$this->treeData[$vid][$item->tid] = $item;
}
}
$this->assertArrayHasKey($tid, $this->treeData[$vid], "Term $tid exists in taxonomy tree");
$term = $this->treeData[$vid][$tid];
$this->assertEquals($parent_ids, array_filter($term->parents), "Term $tid has correct parents in taxonomy tree");
}
/**
* Tests the Drupal 6 i18n taxonomy term to Drupal 8 migration.
*/
public function testTranslatedTaxonomyTerms() {
$this->assertEntity(1, 'zu', 'zu - term 1 of vocabulary 1', 'vocabulary_1_i_0_', 'zu - description of term 1 of vocabulary 1', NULL, '0', []);
$this->assertEntity(2, 'fr', 'fr - term 2 of vocabulary 2', 'vocabulary_2_i_1_', 'fr - description of term 2 of vocabulary 2', NULL, '3', []);
$this->assertEntity(3, 'fr', 'fr - term 3 of vocabulary 2', 'vocabulary_2_i_1_', 'fr - description of term 3 of vocabulary 2', NULL, '4', ['2']);
$this->assertEntity(4, 'en', 'term 4 of vocabulary 3', 'vocabulary_3_i_2_', 'description of term 4 of vocabulary 3', NULL, '6', []);
$this->assertEntity(5, 'en', 'term 5 of vocabulary 3', 'vocabulary_3_i_2_', 'description of term 5 of vocabulary 3', NULL, '7', ['4']);
$this->assertEntity(6, 'en', 'term 6 of vocabulary 3', 'vocabulary_3_i_2_', 'description of term 6 of vocabulary 3', NULL, '8', ['4', '5']);
$this->assertEntity(7, 'fr', 'fr - term 2 of vocabulary 1', 'vocabulary_1_i_0_', 'fr - desc of term 2 vocab 1', NULL, '0', []);
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\Tests\content_translation\Kernel\Migrate\d7;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests the migration of entity translation settings.
*
* @group migrate_drupal_7
*/
class MigrateEntityTranslationSettingsTest extends MigrateDrupal7TestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'comment',
'content_translation',
'language',
'menu_ui',
// Required for translation migrations.
'migrate_drupal_multilingual',
'node',
'taxonomy',
'text',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig([
'comment',
'content_translation',
'node',
'taxonomy',
'user',
]);
$this->installEntitySchema('comment');
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
$this->executeMigrations([
'd7_comment_type',
'd7_node_type',
'd7_taxonomy_vocabulary',
'd7_entity_translation_settings',
]);
}
/**
* Tests entity translation settings migration.
*/
public function testEntityTranslationSettingsMigration() {
// Tests 'comment_node_test_content_type' entity translation settings.
$config = $this->config('language.content_settings.comment.comment_node_test_content_type');
$this->assertSame($config->get('target_entity_type_id'), 'comment');
$this->assertSame($config->get('target_bundle'), 'comment_node_test_content_type');
$this->assertSame($config->get('default_langcode'), 'current_interface');
$this->assertFalse((bool) $config->get('language_alterable'));
$this->assertTrue((bool) $config->get('third_party_settings.content_translation.enabled'));
$this->assertFalse((bool) $config->get('third_party_settings.content_translation.bundle_settings.untranslatable_fields_hide'));
// Tests 'test_content_type' entity translation settings.
$config = $this->config('language.content_settings.node.test_content_type');
$this->assertSame($config->get('target_entity_type_id'), 'node');
$this->assertSame($config->get('target_bundle'), 'test_content_type');
$this->assertSame($config->get('default_langcode'), LanguageInterface::LANGCODE_NOT_SPECIFIED);
$this->assertTrue((bool) $config->get('language_alterable'));
$this->assertTrue((bool) $config->get('third_party_settings.content_translation.enabled'));
$this->assertFalse((bool) $config->get('third_party_settings.content_translation.bundle_settings.untranslatable_fields_hide'));
// Tests 'test_vocabulary' entity translation settings.
$config = $this->config('language.content_settings.taxonomy_term.test_vocabulary');
$this->assertSame($config->get('target_entity_type_id'), 'taxonomy_term');
$this->assertSame($config->get('target_bundle'), 'test_vocabulary');
$this->assertSame($config->get('default_langcode'), LanguageInterface::LANGCODE_SITE_DEFAULT);
$this->assertFalse((bool) $config->get('language_alterable'));
$this->assertTrue((bool) $config->get('third_party_settings.content_translation.enabled'));
$this->assertFalse((bool) $config->get('third_party_settings.content_translation.bundle_settings.untranslatable_fields_hide'));
// Tests 'user' entity translation settings.
$config = $this->config('language.content_settings.user.user');
$this->assertSame($config->get('target_entity_type_id'), 'user');
$this->assertSame($config->get('target_bundle'), 'user');
$this->assertSame($config->get('default_langcode'), LanguageInterface::LANGCODE_SITE_DEFAULT);
$this->assertFalse((bool) $config->get('language_alterable'));
$this->assertTrue((bool) $config->get('third_party_settings.content_translation.enabled'));
$this->assertFalse((bool) $config->get('third_party_settings.content_translation.bundle_settings.untranslatable_fields_hide'));
}
}

View file

@ -0,0 +1,254 @@
<?php
namespace Drupal\Tests\content_translation\Kernel\Plugin\migrate\source\d7;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests entity translation settings source plugin.
*
* @covers \Drupal\content_translation\Plugin\migrate\source\d7\EntityTranslationSettings
*
* @group content_translation
*/
class EntityTranslationSettingsTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'content_translation',
'language',
'migrate_drupal',
];
/**
* {@inheritdoc}
*/
public function providerSource() {
$tests = [];
// Source data when there's no entity type that uses entity translation.
$tests[0]['source_data']['variable'] = [
[
'name' => 'entity_translation_entity_types',
'value' => 'a:4:{s:7:"comment";i:0;s:4:"node";i:0;s:13:"taxonomy_term";i:0;s:4:"user";i:0;}',
],
];
// Source data when there's no bundle settings variables.
$tests[1]['source_data']['variable'] = [
[
'name' => 'entity_translation_entity_types',
'value' => 'a:4:{s:7:"comment";s:7:"comment";s:4:"node";s:4:"node";s:13:"taxonomy_term";s:13:"taxonomy_term";s:4:"user";s:4:"user";}',
],
[
'name' => 'entity_translation_taxonomy',
'value' => 'a:3:{s:6:"forums";b:1;s:4:"tags";b:1;s:4:"test";b:0;}',
],
[
'name' => 'language_content_type_article',
'value' => 's:1:"2";',
],
[
'name' => 'language_content_type_forum',
'value' => 's:1:"4";',
],
[
'name' => 'language_content_type_page',
'value' => 's:1:"4";',
],
];
// Source data when there's bundle settings variables.
$tests[2]['source_data']['variable'] = [
[
'name' => 'entity_translation_entity_types',
'value' => 'a:4:{s:7:"comment";s:7:"comment";s:4:"node";s:4:"node";s:13:"taxonomy_term";s:13:"taxonomy_term";s:4:"user";s:4:"user";}',
],
[
'name' => 'entity_translation_settings_comment__comment_node_forum',
'value' => 'a:5:{s:16:"default_language";s:12:"xx-et-author";s:22:"hide_language_selector";i:1;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:0;}',
],
[
'name' => 'entity_translation_settings_comment__comment_node_page',
'value' => 'a:5:{s:16:"default_language";s:12:"xx-et-author";s:22:"hide_language_selector";i:0;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:1;}',
],
[
'name' => 'entity_translation_settings_node__forum',
'value' => 'a:5:{s:16:"default_language";s:12:"xx-et-author";s:22:"hide_language_selector";i:0;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:0;}',
],
[
'name' => 'entity_translation_settings_node__page',
'value' => 'a:5:{s:16:"default_language";s:13:"xx-et-default";s:22:"hide_language_selector";i:1;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:1;}',
],
[
'name' => 'entity_translation_settings_taxonomy_term__forums',
'value' => 'a:5:{s:16:"default_language";s:13:"xx-et-current";s:22:"hide_language_selector";i:0;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:1;}',
],
[
'name' => 'entity_translation_settings_taxonomy_term__tags',
'value' => 'a:5:{s:16:"default_language";s:13:"xx-et-current";s:22:"hide_language_selector";i:1;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:0;}',
],
[
'name' => 'entity_translation_settings_user__user',
'value' => 'a:5:{s:16:"default_language";s:12:"xx-et-author";s:22:"hide_language_selector";i:1;s:21:"exclude_language_none";i:0;s:13:"lock_language";i:0;s:27:"shared_fields_original_only";i:1;}',
],
[
'name' => 'entity_translation_taxonomy',
'value' => 'a:3:{s:6:"forums";b:1;s:4:"tags";b:1;s:4:"test";b:0;}',
],
[
'name' => 'language_content_type_article',
'value' => 's:1:"2";',
],
[
'name' => 'language_content_type_forum',
'value' => 's:1:"4";',
],
[
'name' => 'language_content_type_page',
'value' => 's:1:"4";',
],
];
// Source data when taxonomy terms are translatable but the
// 'entity_translation_taxonomy' variable is not set.
$tests[3]['source_data']['variable'] = [
[
'name' => 'entity_translation_entity_types',
'value' => 'a:4:{s:7:"comment";i:0;s:4:"node";i:0;s:13:"taxonomy_term";i:1;s:4:"user";i:0;}',
],
];
// Expected data when there's no entity type that uses entity translation.
$tests[0]['expected_data'] = [];
// Expected data when there's no bundle settings variables.
$tests[1]['expected_data'] = [
[
'id' => 'node.forum',
'target_entity_type_id' => 'node',
'target_bundle' => 'forum',
'default_langcode' => 'und',
'language_alterable' => TRUE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'node.page',
'target_entity_type_id' => 'node',
'target_bundle' => 'page',
'default_langcode' => 'und',
'language_alterable' => TRUE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'comment.comment_forum',
'target_entity_type_id' => 'comment',
'target_bundle' => 'comment_forum',
'default_langcode' => 'xx-et-current',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'comment.comment_node_page',
'target_entity_type_id' => 'comment',
'target_bundle' => 'comment_node_page',
'default_langcode' => 'xx-et-current',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'taxonomy_term.forums',
'target_entity_type_id' => 'taxonomy_term',
'target_bundle' => 'forums',
'default_langcode' => 'xx-et-default',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'taxonomy_term.tags',
'target_entity_type_id' => 'taxonomy_term',
'target_bundle' => 'tags',
'default_langcode' => 'xx-et-default',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'user.user',
'target_entity_type_id' => 'user',
'target_bundle' => 'user',
'default_langcode' => 'xx-et-default',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => FALSE,
],
];
// Expected data when there's bundle settings variables.
$tests[2]['expected_data'] = [
[
'id' => 'node.forum',
'target_entity_type_id' => 'node',
'target_bundle' => 'forum',
'default_langcode' => 'xx-et-author',
'language_alterable' => TRUE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'node.page',
'target_entity_type_id' => 'node',
'target_bundle' => 'page',
'default_langcode' => 'xx-et-default',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => TRUE,
],
[
'id' => 'comment.comment_forum',
'target_entity_type_id' => 'comment',
'target_bundle' => 'comment_forum',
'default_langcode' => 'xx-et-author',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'comment.comment_node_page',
'target_entity_type_id' => 'comment',
'target_bundle' => 'comment_node_page',
'default_langcode' => 'xx-et-author',
'language_alterable' => TRUE,
'untranslatable_fields_hide' => TRUE,
],
[
'id' => 'taxonomy_term.forums',
'target_entity_type_id' => 'taxonomy_term',
'target_bundle' => 'forums',
'default_langcode' => 'xx-et-current',
'language_alterable' => TRUE,
'untranslatable_fields_hide' => TRUE,
],
[
'id' => 'taxonomy_term.tags',
'target_entity_type_id' => 'taxonomy_term',
'target_bundle' => 'tags',
'default_langcode' => 'xx-et-current',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => FALSE,
],
[
'id' => 'user.user',
'target_entity_type_id' => 'user',
'target_bundle' => 'user',
'default_langcode' => 'xx-et-author',
'language_alterable' => FALSE,
'untranslatable_fields_hide' => TRUE,
],
];
// Expected data when taxonomy terms are translatable but the
// 'entity_translation_taxonomy' variable is not set.
$tests[3]['expected_data'] = [];
return $tests;
}
}

View file

@ -49,20 +49,30 @@ class ContentTranslationLocalTasksTest extends LocalTaskIntegrationTestBase {
*/
public function providerTestBlockAdminDisplay() {
return [
['entity.node.canonical', [[
'content_translation.local_tasks:entity.node.content_translation_overview',
[
'entity.node.canonical',
'entity.node.edit_form',
'entity.node.delete_form',
'entity.node.version_history',
]]],
['entity.node.content_translation_overview', [[
'content_translation.local_tasks:entity.node.content_translation_overview',
'entity.node.canonical',
'entity.node.edit_form',
'entity.node.delete_form',
'entity.node.version_history',
]]],
[
[
'content_translation.local_tasks:entity.node.content_translation_overview',
'entity.node.canonical',
'entity.node.edit_form',
'entity.node.delete_form',
'entity.node.version_history',
],
],
],
[
'entity.node.content_translation_overview',
[
[
'content_translation.local_tasks:entity.node.content_translation_overview',
'entity.node.canonical',
'entity.node.edit_form',
'entity.node.delete_form',
'entity.node.version_history',
],
],
],
];
}