Commit aeecb0a4 authored by webchick's avatar webchick

Issue #2076445 by plach, andypost, yched, Gábor Hojtsy: Make sure language...

Issue #2076445 by plach, andypost, yched, Gábor Hojtsy: Make sure language codes for original field values always match entity language regardless of field translatability.
parent a57094c8
......@@ -325,7 +325,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
// Get the revision IDs.
$revision_ids = array();
foreach ($entities as $values) {
$revision_ids[] = $values[$this->revisionKey];
$revision_ids[] = $values[$this->revisionKey][Language::LANGCODE_DEFAULT];
}
$query->condition($this->revisionKey, $revision_ids);
}
......@@ -832,12 +832,14 @@ public function getQueryServiceName() {
protected function doLoadFieldItems($entities, $age) {
$load_current = $age == static::FIELD_LOAD_CURRENT;
// Collect entities ids and bundles.
// Collect entities ids, bundles and languages.
$bundles = array();
$ids = array();
$default_langcodes = array();
foreach ($entities as $key => $entity) {
$bundles[$entity->bundle()] = TRUE;
$ids[] = $load_current ? $key : $entity->getRevisionId();
$default_langcodes[$key] = $entity->getUntranslated()->language()->id;
}
// Collect impacted fields.
......@@ -849,14 +851,13 @@ protected function doLoadFieldItems($entities, $age) {
}
// Load field data.
$all_langcodes = array_keys(language_list());
$langcodes = array_keys(language_list(Language::STATE_ALL));
foreach ($fields as $field_name => $field) {
$table = $load_current ? static::_fieldTableName($field) : static::_fieldRevisionTableName($field);
// If the field is translatable ensure that only values having valid
// languages are retrieved. Since we are loading values for multiple
// entities, we cannot limit the query to the available translations.
$langcodes = $field->isFieldTranslatable() ? $all_langcodes : array(Language::LANGCODE_NOT_SPECIFIED);
// Ensure that only values having valid languages are retrieved. Since we
// are loading values for multiple entities, we cannot limit the query to
// the available translations.
$results = $this->database->select($table, 't')
->fields('t')
->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN')
......@@ -867,23 +868,27 @@ protected function doLoadFieldItems($entities, $age) {
$delta_count = array();
foreach ($results as $row) {
if (!isset($delta_count[$row->entity_id][$row->langcode])) {
$delta_count[$row->entity_id][$row->langcode] = 0;
}
if ($field->getFieldCardinality() == FieldInterface::CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $field->getFieldCardinality()) {
$item = array();
// For each column declared by the field, populate the item from the
// prefixed database column.
foreach ($field->getColumns() as $column => $attributes) {
$column_name = static::_fieldColumnName($field, $column);
// Unserialize the value if specified in the column schema.
$item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
// Ensure that records for non-translatable fields having invalid
// languages are skipped.
if ($row->langcode == $default_langcodes[$row->entity_id] || $field->isFieldTranslatable()) {
if (!isset($delta_count[$row->entity_id][$row->langcode])) {
$delta_count[$row->entity_id][$row->langcode] = 0;
}
// Add the item to the field values for the entity.
$entities[$row->entity_id]->getTranslation($row->langcode)->{$field_name}[$delta_count[$row->entity_id][$row->langcode]] = $item;
$delta_count[$row->entity_id][$row->langcode]++;
if ($field->getFieldCardinality() == FieldInterface::CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $field->getFieldCardinality()) {
$item = array();
// For each column declared by the field, populate the item from the
// prefixed database column.
foreach ($field->getColumns() as $column => $attributes) {
$column_name = static::_fieldColumnName($field, $column);
// Unserialize the value if specified in the column schema.
$item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
}
// Add the item to the field values for the entity.
$entities[$row->entity_id]->getTranslation($row->langcode)->{$field_name}[$delta_count[$row->entity_id][$row->langcode]] = $item;
$delta_count[$row->entity_id][$row->langcode]++;
}
}
}
}
......@@ -897,6 +902,9 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) {
$id = $entity->id();
$bundle = $entity->bundle();
$entity_type = $entity->entityType();
$default_langcode = $entity->getUntranslated()->language()->id;
$translation_langcodes = array_keys($entity->getTranslationLanguages());
if (!isset($vid)) {
$vid = $id;
}
......@@ -930,7 +938,7 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) {
$query = $this->database->insert($table_name)->fields($columns);
$revision_query = $this->database->insert($revision_name)->fields($columns);
$langcodes = $field->isFieldTranslatable() ? array_keys($entity->getTranslationLanguages()) : array(Language::LANGCODE_NOT_SPECIFIED);
$langcodes = $field->isFieldTranslatable() ? $translation_langcodes : array($default_langcode);
foreach ($langcodes as $langcode) {
$delta_count = 0;
$items = $entity->getTranslation($langcode)->get($field_name);
......
......@@ -35,7 +35,7 @@ class FieldItemList extends ItemList implements FieldItemListInterface {
*
* @var string
*/
protected $langcode = Language::LANGCODE_DEFAULT;
protected $langcode = Language::LANGCODE_NOT_SPECIFIED;
/**
* {@inheritdoc}
......
......@@ -60,10 +60,10 @@ class Language {
/**
* Language code referring to the default language of data, e.g. of an entity.
*
* @todo: Change value to differ from Language::LANGCODE_NOT_SPECIFIED once
* field API leverages the property API.
* See the BCP 47 syntax for defining private language tags:
* http://www.rfc-editor.org/rfc/bcp/bcp47.txt
*/
const LANGCODE_DEFAULT = 'und';
const LANGCODE_DEFAULT = 'x-default';
/**
* The language state when referring to configurable languages.
......
......@@ -312,7 +312,7 @@ function aggregator_update_8001() {
'length' => 12,
'not null' => TRUE,
'default' => '',
'initial' => Language::LANGCODE_DEFAULT,
'initial' => Language::LANGCODE_NOT_SPECIFIED,
));
db_add_field('aggregator_item', 'langcode', array(
'description' => 'The {language}.langcode of this feed item.',
......@@ -320,6 +320,6 @@ function aggregator_update_8001() {
'length' => 12,
'not null' => TRUE,
'default' => '',
'initial' => Language::LANGCODE_DEFAULT,
'initial' => Language::LANGCODE_NOT_SPECIFIED,
));
}
......@@ -331,9 +331,6 @@ function content_translation_form_language_content_settings_submit(array $form,
* language_save_default_configuration().
* - fields: An associative array with field names as keys and a boolean as
* value, indicating field translatability.
*
* @todo Remove this migration entirely once the Field API is converted to the
* Entity Field API.
*/
function _content_translation_update_field_translatability($settings) {
$fields = array();
......@@ -346,224 +343,15 @@ function _content_translation_update_field_translatability($settings) {
foreach ($bundle_settings['fields'] as $field_name => $translatable) {
// If a field is enabled for translation for at least one instance we
// need to mark it as translatable.
if (FieldService::fieldInfo()->getField($entity_type, $field_name)) {
$fields[$entity_type][$field_name] = $translatable || !empty($fields[$entity_type][$field_name]);
$field = FieldService::fieldInfo()->getField($entity_type, $field_name);
if ($field && $field->isFieldTranslatable() !== $translatable) {
$field->translatable = $translatable;
$field->save();
}
}
}
}
}
$operations = array();
foreach ($fields as $entity_type => $entity_type_fields) {
foreach ($entity_type_fields as $field_name => $translatable) {
$field = field_info_field($entity_type, $field_name);
if ($field->isFieldTranslatable() != $translatable) {
// If a field is untranslatable, it can have no data except under
// Language::LANGCODE_NOT_SPECIFIED. Thus we need a field to be translatable before
// we convert data to the entity language. Conversely we need to switch
// data back to Language::LANGCODE_NOT_SPECIFIED before making a field
// untranslatable lest we lose information.
$field_operations = array(
array('content_translation_translatable_switch', array($translatable, $entity_type, $field_name)),
);
if ($field->hasData()) {
$field_operations[] = array('content_translation_translatable_batch', array($translatable, $field_name));
$field_operations = $translatable ? $field_operations : array_reverse($field_operations);
}
$operations = array_merge($operations, $field_operations);
}
}
}
// As last operation store the submitted settings.
$operations[] = array('content_translation_save_settings', array($settings));
$batch = array(
'title' => t('Updating translatability for the selected fields'),
'operations' => $operations,
'finished' => 'content_translation_translatable_batch_done',
'file' => drupal_get_path('module', 'content_translation') . '/content_translation.admin.inc',
);
batch_set($batch);
}
/**
* Toggles translatability of the given field.
*
* This is called from a batch operation, but should only run once per field.
*
* @param bool $translatable
* Indicator of whether the field should be made translatable (TRUE) or
* untranslatble (FALSE).
* @param string $entity_type
* Field entity type.
* @param string $field_name
* Field machine name.
*/
function content_translation_translatable_switch($translatable, $entity_type, $field_name) {
$field = field_info_field($entity_type, $field_name);
if ($field->isFieldTranslatable() !== $translatable) {
$field->translatable = $translatable;
$field->save();
}
content_translation_save_settings($settings);
}
/**
* Batch callback: Converts field data to or from Language::LANGCODE_NOT_SPECIFIED.
*
* @param bool $translatable
* Indicator of whether the field should be made translatable (TRUE) or
* untranslatble (FALSE).
* @param string $field_name
* Field machine name.
*/
function content_translation_translatable_batch($translatable, $field_name, &$context) {
// Determine the entity types to act on.
$entity_types = array();
foreach (field_info_instances() as $entity_type => $info) {
foreach ($info as $bundle => $instances) {
foreach ($instances as $instance_field_name => $instance) {
if ($instance_field_name == $field_name) {
$entity_types[] = $entity_type;
break 2;
}
}
}
}
if (empty($context['sandbox'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = 0;
foreach ($entity_types as $entity_type) {
$field = field_info_field($entity_type, $field_name);
$columns = $field->getColumns();
$column = isset($columns['value']) ? 'value' : key($columns);
$query_field = "$field_name.$column";
// How many entities will need processing?
$query = \Drupal::entityQuery($entity_type);
$count = $query
->exists($query_field)
->count()
->execute();
$context['sandbox']['max'] += $count;
$context['sandbox']['progress_entity_type'][$entity_type] = 0;
$context['sandbox']['max_entity_type'][$entity_type] = $count;
}
if ($context['sandbox']['max'] === 0) {
// Nothing to do.
$context['finished'] = 1;
return;
}
}
foreach ($entity_types as $entity_type) {
if ($context['sandbox']['max_entity_type'][$entity_type] === 0) {
continue;
}
$info = entity_get_info($entity_type);
$offset = $context['sandbox']['progress_entity_type'][$entity_type];
$query = \Drupal::entityQuery($entity_type);
$field = field_info_field($entity_type, $field_name);
$columns = $field->getColumns();
$column = isset($columns['value']) ? 'value' : key($columns);
$query_field = "$field_name.$column";
$result = $query
->exists($query_field)
->sort($info['entity_keys']['id'])
->range($offset, 10)
->execute();
foreach (entity_load_multiple($entity_type, $result) as $id => $entity) {
$context['sandbox']['max_entity_type'][$entity_type] -= count($result);
$context['sandbox']['progress_entity_type'][$entity_type]++;
$context['sandbox']['progress']++;
$langcode = $entity->language()->id;
// Skip process for language neutral entities.
if ($langcode == Language::LANGCODE_NOT_SPECIFIED) {
continue;
}
// We need a two-step approach while updating field translations: given
// that field-specific update functions might rely on the stored values to
// perform their processing first we need to store the new translations
// and only after we can remove the old ones. Otherwise we might have data
// loss, since the removal of the old translations might occur before the
// new ones are stored.
if ($translatable && isset($entity->{$field_name}[Language::LANGCODE_NOT_SPECIFIED])) {
// If the field is being switched to translatable and has data for
// Language::LANGCODE_NOT_SPECIFIED then we need to move the data to the right
// language.
$entity->{$field_name}[$langcode] = $entity->{$field_name}[Language::LANGCODE_NOT_SPECIFIED];
// Store the original value.
_content_translation_update_field($entity_type, $entity, $field_name);
$entity->{$field_name}[Language::LANGCODE_NOT_SPECIFIED] = array();
// Remove the language neutral value.
_content_translation_update_field($entity_type, $entity, $field_name);
}
elseif (!$translatable && isset($entity->{$field_name}[$langcode])) {
// The field has been marked untranslatable and has data in the entity
// language: we need to move it to Language::LANGCODE_NOT_SPECIFIED and drop the
// other translations.
$entity->{$field_name}[Language::LANGCODE_NOT_SPECIFIED] = $entity->{$field_name}[$langcode];
// Store the original value.
_content_translation_update_field($entity_type, $entity, $field_name);
// Remove translations.
foreach ($entity->{$field_name} as $langcode => $items) {
if ($langcode != Language::LANGCODE_NOT_SPECIFIED) {
$entity->{$field_name}[$langcode] = array();
}
}
_content_translation_update_field($entity_type, $entity, $field_name);
}
else {
// No need to save unchanged entities.
continue;
}
}
}
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
/**
* Stores the given field translations.
*/
function _content_translation_update_field($entity_type, EntityInterface $entity, $field_name) {
$empty = 0;
$translations = $entity->getTranslationLanguages();
// Ensure that we are trying to store only valid data.
foreach (array_keys($translations) as $langcode) {
$items = $entity->getTranslation($langcode)->get($field_name);
$items->filterEmptyValues();
$empty += $items->isEmpty();
}
// Save the field value only if there is at least one item available,
// otherwise any stored empty field value would be deleted. If this happens
// the range queries would be messed up.
if ($empty < count($translations)) {
$entity->save();
}
}
/**
* Batch finished callback: Checks the exit status of the batch operation.
*/
function content_translation_translatable_batch_done($success, $results, $operations) {
if ($success) {
drupal_set_message(t("Successfully changed field translation setting."));
}
else {
// @todo: Do something about this case.
drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."), 'error');
}
}
......@@ -80,10 +80,6 @@ function content_translation_entity_info_alter(array &$entity_info) {
// Provide defaults for translation info.
foreach ($entity_info as $entity_type => &$info) {
if (!empty($info['translatable'])) {
// Every fieldable entity type must have a translation controller class,
// no matter if it is enabled for translation or not. As a matter of fact
// we might need it to correctly switch field translatability when a field
// is shared accross different entities.
$info['controllers'] += array('translation' => 'Drupal\content_translation\ContentTranslationController');
if (!isset($info['translation']['content_translation'])) {
......@@ -223,12 +219,6 @@ function content_translation_menu() {
}
}
$items['admin/config/regional/content_translation/translatable/%/%'] = array(
'title' => 'Confirm change in translatability.',
'description' => 'Confirm page for changing field translatability.',
'route_name' => 'content_translation.translatable',
);
return $items;
}
......@@ -817,36 +807,12 @@ function content_translation_field_extra_fields() {
* Implements hook_form_FORM_ID_alter() for 'field_ui_field_edit_form'.
*/
function content_translation_form_field_ui_field_edit_form_alter(array &$form, array &$form_state, $form_id) {
$field = $form['#field'];
$field_name = $field->getFieldName();
$translatable = $field->isFieldTranslatable();
$entity_type = $field->entity_type;
$label = t('Field translation');
if ($field->hasData()) {
$form['field']['translatable'] = array(
'#type' => 'item',
'#title' => $label,
'#attributes' => array('class' => 'translatable'),
'link' => array(
'#type' => 'link',
'#prefix' => t('This field has data in existing content.') . ' ',
'#title' => !$translatable ? t('Enable translation') : t('Disable translation'),
'#href' => "admin/config/regional/content_translation/translatable/$entity_type/$field_name",
'#options' => array('query' => drupal_get_destination()),
'#access' => user_access('administer content translation'),
),
);
}
else {
$form['field']['translatable'] = array(
'#type' => 'checkbox',
'#title' => t('Users may translate this field.'),
'#default_value' => $translatable,
);
}
$form['field']['translatable']['#weight'] = 20;
$form['field']['translatable'] = array(
'#type' => 'checkbox',
'#title' => t('Users may translate this field.'),
'#default_value' => $form['#field']->isFieldTranslatable(),
'#weight' => 20,
);
}
/**
......
content_translation.translatable:
path: '/admin/config/regional/content_translation/translatable/{entity_type}/{field_name}'
defaults:
_form: 'Drupal\content_translation\Form\TranslatableForm'
requirements:
_permission: 'administer content translation'
<?php
/**
* @file
* Contains \Drupal\content_translation\Form\TranslatableForm.
*/
namespace Drupal\content_translation\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\field\Entity\Field;
use Drupal\field\Field as FieldInfo;
/**
* Provides a confirm form for changing translatable status on translation
* fields.
*/
class TranslatableForm extends ConfirmFormBase {
/**
* The field info we are changing translatable status on.
*
* @var \Drupal\field\Entity\Field
*/
protected $field;
/**
* The field name we are changing translatable
* status on.
*
* @var string.
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'content_translation_translatable_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
if ($this->field->isFieldTranslatable()) {
$question = t('Are you sure you want to disable translation for the %name field?', array('%name' => $this->fieldName));
}
else {
$question = t('Are you sure you want to enable translation for the %name field?', array('%name' => $this->fieldName));
}
return $question;
}
/**
* {@inheritdoc}
*/
public function getDescription() {
$description = t('By submitting this form these changes will apply to the %name field everywhere it is used.',
array('%name' => $this->fieldName)
);
$description .= $this->field->isFieldTranslatable() ? "<br>" . t("<strong>All the existing translations of this field will be deleted.</strong><br>This action cannot be undone.") : '';
return $description;
}
/**
* {@inheritdoc}
*/
public function getCancelRoute() {
return array(
'route_name' => '<front>',
);
}
/**
* {@inheritdoc}
* @param string $entity_type
* The entity type.
* @param string $field_name
* The field name.
*/
public function buildForm(array $form, array &$form_state, $entity_type = NULL, $field_name = NULL) {
$this->fieldName = $field_name;
$this->fieldInfo = FieldInfo::fieldInfo()->getField($entity_type, $field_name);
return parent::buildForm($form, $form_state);
}
/**
* Form submission handler.
*
* This submit handler maintains consistency between the translatability of an
* entity and the language under which the field data is stored. When a field
* is marked as translatable, all the data in
* $entity->{field_name}[Language::LANGCODE_NOT_SPECIFIED] is moved to
* $entity->{field_name}[$entity_language]. When a field is marked as
* untranslatable the opposite process occurs. Note that marking a field as
* untranslatable will cause all of its translations to be permanently
* removed, with the exception of the one corresponding to the entity
* language.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* An associative array containing the current state of the form.
*/
public function submitForm(array &$form, array &$form_state) {
// This is the current state that we want to reverse.
$translatable = $form_state['values']['translatable'];
if ($this->field->translatable !== $translatable) {
// Field translatability has changed since form creation, abort.
$t_args = array('%field_name');
$msg = $translatable ?
t('The field %field_name is already translatable. No change was performed.', $t_args):
t('The field %field_name is already untranslatable. No change was performed.', $t_args);
drupal_set_message($msg, 'warning');
return;
}
// If a field is untranslatable, it can have no data except under
// Language::LANGCODE_NOT_SPECIFIED. Thus we need a field to be translatable
// before we convert data to the entity language. Conversely we need to
// switch data back to Language::LANGCODE_NOT_SPECIFIED before making a
// field untranslatable lest we lose information.
$operations = array(
array(
'content_translation_translatable_batch', array(
!$translatable,
$this->fieldName,
),
),
array(
'content_translation_translatable_switch', array(
!$translatable,
$this->field['entity_type'],
$this->fieldName,
),
),
);
$operations = $translatable ? $operations : array_reverse($operations);