Commit e6ce24be authored by alexpott's avatar alexpott

Issue #2496867 by Berdir, swentel, alexpott, rodrigoaguilera, yobottehg,...

Issue #2496867 by Berdir, swentel, alexpott, rodrigoaguilera, yobottehg, trebormc: Translatable image file is not working unless you also config the image field. Config can get lost anyway
parent 2b6be926
......@@ -19,11 +19,14 @@
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* A field definition object.
* @param string $element_name
* (optional) The element name, which is added to drupalSettings so that
* javascript can manipulate the form element.
*
* @return array
* A form element to configure field synchronization.
*/
function content_translation_field_sync_widget(FieldDefinitionInterface $field) {
function content_translation_field_sync_widget(FieldDefinitionInterface $field, $element_name = 'third_party_settings[content_translation][translation_sync]') {
// No way to store field sync information on this field.
if (!($field instanceof ThirdPartySettingsInterface)) {
return array();
......@@ -33,15 +36,18 @@ function content_translation_field_sync_widget(FieldDefinitionInterface $field)
$definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->getType());
$column_groups = $definition['column_groups'];
if (!empty($column_groups) && count($column_groups) > 1) {
$options = array();
$default = array();
$options = [];
$default = [];
$require_all_groups_for_translation = [];
foreach ($column_groups as $group => $info) {
$options[$group] = $info['label'];
$default[$group] = !empty($info['translatable']) ? $group : FALSE;
if (!empty($info['require_all_groups_for_translation'])) {
$require_all_groups_for_translation[] = $group;
}
}
$settings = array('dependent_selectors' => array('instance[third_party_settings][content_translation][translation_sync]' => array('file')));
$default = $field->getThirdPartySetting('content_translation', 'translation_sync', $default);
$element = array(
......@@ -49,15 +55,19 @@ function content_translation_field_sync_widget(FieldDefinitionInterface $field)
'#title' => t('Translatable elements'),
'#options' => $options,
'#default_value' => $default,
'#attached' => array(
'library' => array(
'content_translation/drupal.content_translation.admin',
),
'drupalSettings' => [
'contentTranslationDependentOptions' => $settings,
],
),
);
if ($require_all_groups_for_translation) {
// The actual checkboxes are sometimes rendered separately and the parent
// element is ignored. Attach to the first option to ensure that this
// does not get lost.
$element[key($options)]['#attached']['drupalSettings']['contentTranslationDependentOptions'] = [
'dependent_selectors' => [
$element_name => $require_all_groups_for_translation
],
];
$element[key($options)]['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
}
}
return $element;
......@@ -82,7 +92,6 @@ function _content_translation_form_language_content_settings_form_alter(array &$
$form['#attached']['library'][] = 'content_translation/drupal.content_translation.admin';
$dependent_options_settings = array();
$entity_manager = Drupal::entityManager();
foreach ($form['#labels'] as $entity_type_id => $label) {
$entity_type = $entity_manager->getDefinition($entity_type_id);
......@@ -110,13 +119,9 @@ function _content_translation_form_language_content_settings_form_alter(array &$
'#default_value' => $definition->isTranslatable(),
);
// Display the column translatability configuration widget.
$column_element = content_translation_field_sync_widget($definition);
$column_element = content_translation_field_sync_widget($definition, "settings[{$entity_type_id}][{$bundle}][columns][{$field_name}]");
if ($column_element) {
$form['settings'][$entity_type_id][$bundle]['columns'][$field_name] = $column_element;
// @todo This should not concern only files.
if (isset($column_element['#options']['file'])) {
$dependent_options_settings["settings[{$entity_type_id}][{$bundle}][columns][{$field_name}]"] = array('file');
}
}
}
}
......@@ -132,8 +137,6 @@ function _content_translation_form_language_content_settings_form_alter(array &$
}
}
$settings = array('dependent_selectors' => $dependent_options_settings);
$form['#attached']['drupalSettings']['contentTranslationDependentOptions'] = $settings;
$form['#validate'][] = 'content_translation_form_language_content_settings_validate';
$form['#submit'][] = 'content_translation_form_language_content_settings_submit';
}
......
......@@ -31,7 +31,7 @@
// 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.dependent_selectors) {
if (options && options.dependent_selectors) {
for (var field in options.dependent_selectors) {
if (options.dependent_selectors.hasOwnProperty(field)) {
$fields = $context.find('input[name^="' + field + '"]');
......
......@@ -45,3 +45,19 @@ function content_translation_update_8001() {
/**
* @} End of "addtogroup updates-8.0.0-rc".
*/
/**
* @addtogroup updates-8.0.x
* @{
*/
/**
* Clear field type plugin caches to fix image field translatability.
*/
function content_translation_update_8002() {
\Drupal::service('plugin.manager.field.field_type')->clearCachedDefinitions();
}
/**
* @} End of "addtogroup updates-8.0.x".
*/
......@@ -14,6 +14,7 @@
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\FieldConfigInterface;
/**
* Implements hook_help().
......@@ -199,9 +200,17 @@ function content_translation_entity_base_field_info(EntityTypeInterface $entity_
* which columns should be synchronized across different translations and
* which are translatable. This is useful for instance to translate the
* "alt" and "title" textual elements of an image field, while keeping the
* same image on every translation.
* same image on every translation. Each group has the following keys:
* - title: Title of the column group.
* - translatable: (optional) If the column group should be translatable by
* default, defaults to FALSE.
* - columns: (optional) A list of columns of this group. Defaults to the
* name of he group as the single column.
* - require_all_groups_for_translation: (optional) Set to TRUE to enforce
* that making this column group translatable requires all others to be
* translatable too.
*
* @see Drupal\image\Plugin\Field\FieldType\imageItem.
* @see Drupal\image\Plugin\Field\FieldType\ImageItem
*/
function content_translation_field_info_alter(&$info) {
foreach ($info as $key => $settings) {
......
......@@ -66,6 +66,17 @@ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode
// Retrieve all the untranslatable column groups and merge them into
// single list.
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
// If a group was selected has the require_all_groups_for_translation
// flag set, there are no untranslatable columns. This is done because
// the UI adds Javascript that disables the other checkboxes, so their
// values are not saved.
foreach (array_filter($translation_sync) as $group) {
if (!empty($column_groups[$group]['require_all_groups_for_translation'])) {
$groups = [];
break;
}
}
if (!empty($groups)) {
$columns = array();
foreach ($groups as $group) {
......@@ -163,7 +174,16 @@ public function synchronizeItems(array &$values, array $unchanged_items, $sync_l
continue;
}
// If a synchronized column has changed or has been created from
// scratch we need to override the full items array for all languages.
// scratch we need to replace the values for this language as a
// combination of the values that need to be synced from the source
// items and the other columns from the existing values. This only
// works if the delta exists in the language.
elseif ($created && !empty($original_field_values[$langcode][$delta])) {
$item_columns_to_sync = array_intersect_key($source_items[$delta], array_flip($columns));
$item_columns_to_keep = array_diff_key($original_field_values[$langcode][$delta], array_flip($columns));
$values[$langcode][$delta] = $item_columns_to_sync + $item_columns_to_keep;
}
// If the delta doesn't exist, copy from the source language.
elseif ($created) {
$values[$langcode][$delta] = $source_items[$delta];
}
......
......@@ -201,7 +201,9 @@ function($delta) { return $delta === 0 || $delta === 3; },
for ($delta = 0; $delta < $this->cardinality; $delta++) {
if ($delta_callback($delta)) {
foreach ($this->columns as $column) {
$field_values[$sync_langcode][$delta][$column] = $field_values[$sync_langcode][0][$column];
if (in_array($column, $this->synchronized)) {
$field_values[$sync_langcode][$delta][$column] = $field_values[$sync_langcode][0][$column];
}
}
}
}
......@@ -209,19 +211,18 @@ function($delta) { return $delta === 0 || $delta === 3; },
$changed_items = $field_values[$sync_langcode];
$this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
$result = TRUE;
foreach ($this->unchangedFieldValues as $langcode => $unchanged_items) {
for ($delta = 0; $delta < $this->cardinality; $delta++) {
foreach ($this->columns as $column) {
// The first item is always unchanged hence it is retained by the
// synchronization process. The other ones are retained or synced
// depending on the logic implemented by the delta callback.
$value = $delta > 0 && $delta_callback($delta) ? $changed_items[0][$column] : $unchanged_items[$delta][$column];
$result = $result && ($field_values[$langcode][$delta][$column] == $value);
// depending on the logic implemented by the delta callback and
// whether it is a sync column or not.
$value = $delta > 0 && $delta_callback($delta) && in_array($column, $this->synchronized) ? $changed_items[0][$column] : $unchanged_items[$delta][$column];
$this->assertEqual($field_values[$langcode][$delta][$column], $value, "Item $delta column $column for langcode $langcode synced correctly");
}
}
}
$this->assertTrue($result, 'Multiple synced items have been correctly synchronized.');
}
}
......@@ -241,15 +242,16 @@ public function testDifferingSyncedColumns() {
$changed_items = $field_values[$sync_langcode];
$this->synchronizer->synchronizeItems($field_values, $unchanged_items, $sync_langcode, $this->langcodes, $this->synchronized);
$result = TRUE;
foreach ($this->unchangedFieldValues as $langcode => $unchanged_items) {
for ($delta = 0; $delta < $this->cardinality; $delta++) {
foreach ($this->columns as $column) {
$result = $result && ($field_values[$langcode][$delta][$column] == $changed_items[$delta][$column]);
// If the column is synchronized, the value should have been synced,
// for unsychronized 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");
}
}
}
$this->assertTrue($result, 'Differing synced columns have been correctly synchronized.');
}
}
......@@ -31,7 +31,8 @@
* "label" = @Translation("File"),
* "columns" = {
* "target_id", "width", "height"
* }
* },
* "require_all_groups_for_translation" = TRUE
* },
* "alt" = {
* "label" = @Translation("Alt"),
......
......@@ -159,4 +159,11 @@ function uploadNodeImage($image, $field_name, $type, $alt = '') {
return isset($matches[1]) ? $matches[1] : FALSE;
}
/**
* Retrieves the fid of the last inserted file.
*/
protected function getLastFileId() {
return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField();
}
}
<?php
/**
* @file
* Contains \Drupal\image\Tests\ImageOnTranslatedEntityTest.
*/
namespace Drupal\image\Tests;
use Drupal\file\Entity\File;
/**
* Uploads images to translated nodes.
*
* @group image
*/
class ImageOnTranslatedEntityTest extends ImageFieldTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('language', 'content_translation', 'field_ui');
/**
* The name of the image field used in the test.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create the "Basic page" node type.
$this->drupalCreateContentType(array('type' => 'basicpage', 'name' => 'Basic page'));
// Create a image field on the "Basic page" node type.
$this->fieldName = strtolower($this->randomMachineName());
$this->createImageField($this->fieldName, 'basicpage', [], ['title_field' => 1]);
// Create and login user.
$permissions = array(
'access administration pages',
'administer content translation',
'administer content types',
'administer languages',
'administer node fields',
'create content translations',
'create basicpage content',
'edit any basicpage content',
'translate any entity',
'delete any basicpage content',
);
$admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($admin_user);
// Add a second and third language.
$edit = array();
$edit['predefined_langcode'] = 'fr';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$edit = array();
$edit['predefined_langcode'] = 'nl';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
}
/**
* Tests synced file fields on translated nodes.
*/
public function testSyncedImages() {
// Enable translation for "Basic page" nodes.
$edit = array(
'entity_types[node]' => 1,
'settings[node][basicpage][translatable]' => 1,
"settings[node][basicpage][fields][$this->fieldName]" => 1,
"settings[node][basicpage][columns][$this->fieldName][file]" => 1,
// Explicitly disable alt and title since the javascript disables the
// checkboxes on the form.
"settings[node][basicpage][columns][$this->fieldName][alt]" => FALSE,
"settings[node][basicpage][columns][$this->fieldName][title]" => FALSE,
);
$this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
// Verify that the image field on the "Basic basic" node type is
// translatable.
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'basicpage');
$this->assertTrue($definitions[$this->fieldName]->isTranslatable(), 'Node image field is translatable.');
// Create a default language node.
$default_language_node = $this->drupalCreateNode(array('type' => 'basicpage', 'title' => 'Lost in translation'));
// Edit the node to upload a file.
$edit = array();
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('image')[0]->uri);
$this->drupalPostForm('node/' . $default_language_node->id() . '/edit', $edit, t('Save'));
$edit = [$this->fieldName . '[0][alt]' => 'Lost in translation image', $this->fieldName . '[0][title]' => 'Lost in translation image title'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$first_fid = $this->getLastFileId();
// Translate the node into French: remove the existing file.
$this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/fr', array(), t('Remove'));
// Upload a different file.
$edit = array();
$edit['title[0][value]'] = 'Scarlett Johansson';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('image')[1]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$edit = [$this->fieldName . '[0][alt]' => 'Scarlett Johansson image', $this->fieldName . '[0][title]' => 'Scarlett Johansson image title'];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
// This inspects the HTML after the post of the translation, the image
// should be displayed on the original node.
$this->assertRaw('alt="Lost in translation image"');
$this->assertRaw('title="Lost in translation image title"');
$second_fid = $this->getLastFileId();
// View the translated node.
$this->drupalGet('fr/node/' . $default_language_node->id());
$this->assertRaw('alt="Scarlett Johansson image"');
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
/* @var $file \Drupal\file\FileInterface */
// Ensure the file status of the first file permanent.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the second file is permanent.
$file = File::load($second_fid);
$this->assertTrue($file->isPermanent());
// Translate the node into dutch: remove the existing file.
$this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/nl', array(), t('Remove'));
// Upload a different file.
$edit = array();
$edit['title[0][value]'] = 'Akiko Takeshita';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('image')[2]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$edit = [$this->fieldName . '[0][alt]' => 'Akiko Takeshita image', $this->fieldName . '[0][title]' => 'Akiko Takeshita image title'];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$third_fid = $this->getLastFileId();
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first file is untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
// This inspects the HTML after the post of the translation, the image
// should be displayed on the original node.
$this->assertRaw('alt="Lost in translation image"');
$this->assertRaw('title="Lost in translation image title"');
// View the translated node.
$this->drupalGet('nl/node/' . $default_language_node->id());
$this->assertRaw('alt="Akiko Takeshita image"');
$this->assertRaw('title="Akiko Takeshita image title"');
// Ensure the file status of the second file is permanent.
$file = File::load($second_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the third file is permanent.
$file = File::load($third_fid);
$this->assertTrue($file->isPermanent());
// Edit the second translation: remove the existing file.
$this->drupalPostForm('fr/node/' . $default_language_node->id() . '/edit', array(), t('Remove'));
// Upload a different file.
$edit = array();
$edit['title[0][value]'] = 'Giovanni Ribisi';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('image')[3]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$name = $this->fieldName . '[0][alt]';
$edit = [$name => 'Giovanni Ribisi image'];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$replaced_second_fid = $this->getLastFileId();
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first and third files are untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
$file = File::load($third_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the replaced second file is permanent.
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the old second file is now temporary.
$file = File::load($second_fid);
$this->assertTrue($file->isTemporary());
// Delete the third translation.
$this->drupalPostForm('nl/node/' . $default_language_node->id() . '/delete', array(), t('Delete Dutch translation'));
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first and replaced second files are untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the third file is now temporary.
$file = File::load($third_fid);
$this->assertTrue($file->isTemporary());
// Delete the all translations.
$this->drupalPostForm('node/' . $default_language_node->id() . '/delete', array(), t('Delete all translations'));
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the file status of the all files are now temporary.
$file = File::load($first_fid);
$this->assertTrue($file->isTemporary(), 'First file still exists and is temporary.');
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isTemporary());
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment