diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 5447e90..82fbe7a 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -154,10 +154,10 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans // original language. $data = array('status' => static::TRANSLATION_EXISTING); $this->translations[Language::LANGCODE_DEFAULT] = $data; + $this->initializeDefaultLanguage(); if ($translations) { - $default_langcode = $this->language()->id; foreach ($translations as $langcode) { - if ($langcode != $default_langcode && $langcode != Language::LANGCODE_DEFAULT) { + if ($langcode != $this->language->id && $langcode != Language::LANGCODE_DEFAULT) { $this->translations[$langcode] = $data; } } @@ -399,7 +399,8 @@ protected function getTranslatedField($property_name, $langcode) { } // Non-translatable fields are always stored with // Language::LANGCODE_DEFAULT as key. - if ($langcode != Language::LANGCODE_DEFAULT && empty($definition['translatable'])) { + $default = $langcode == Language::LANGCODE_DEFAULT; + if (!$default && empty($definition['translatable'])) { if (!isset($this->fields[$property_name][Language::LANGCODE_DEFAULT])) { $this->fields[$property_name][Language::LANGCODE_DEFAULT] = $this->getTranslatedField($property_name, Language::LANGCODE_DEFAULT); } @@ -411,7 +412,14 @@ protected function getTranslatedField($property_name, $langcode) { $value = $this->values[$property_name][$langcode]; } $field = \Drupal::typedData()->getPropertyInstance($this, $property_name, $value); - $field->setLangcode($langcode); + if (!$default) { + $field->setLangcode($langcode); + } + // If we are initializing the default language cache, the variable is + // not populated, thus we have no valid value to set. + elseif (isset($this->language)) { + $field->setLangcode($this->language->id); + } $this->fields[$property_name][$langcode] = $field; } } @@ -537,17 +545,17 @@ public function language() { return $this->languages[$this->activeLangcode]; } else { - return $this->language ?: $this->getDefaultLanguage(); + return $this->language; } } /** - * Returns the entity original language. + * Initializes the entity original language local cache. * * @return \Drupal\Core\Language\Language * A language object. */ - protected function getDefaultLanguage() { + protected function initializeDefaultLanguage() { // Keep a local cache of the language object and clear it if the langcode // gets changed, see ContentEntityBase::onChange(). if (!isset($this->language)) { @@ -559,11 +567,32 @@ protected function getDefaultLanguage() { // Make sure we return a proper language object. $this->language = new Language(array('id' => Language::LANGCODE_NOT_SPECIFIED, 'locked' => TRUE)); } + // This needs to be initialized manually as it is skipped when + // instantiating the language field object to avoid infinite recursion. + if (!empty($this->fields['langcode'])) { + $this->fields['langcode'][Language::LANGCODE_DEFAULT]->setLangcode($this->language->id); + } } return $this->language; } /** + * Updates language for already instantiated fields. + * + * @return \Drupal\Core\Language\Language + * A language object. + */ + protected function updateFieldLangcodes($langcode) { + if (!empty($this->fields)) { + foreach ($this->fields as $name => $items) { + if (!empty($items[Language::LANGCODE_DEFAULT])) { + $items[Language::LANGCODE_DEFAULT]->setLangcode($langcode); + } + } + } + } + + /** * {@inheritdoc} */ public function onChange($property_name) { @@ -571,6 +600,8 @@ public function onChange($property_name) { // Avoid using unset as this unnecessarily triggers magic methods later // on. $this->language = NULL; + $this->initializeDefaultLanguage(); + $this->updateFieldLangcodes($this->language->id); } } @@ -582,11 +613,8 @@ public function onChange($property_name) { public function getTranslation($langcode) { // Ensure we always use the default language code when dealing with the // original entity language. - if ($langcode != Language::LANGCODE_DEFAULT) { - $default_language = $this->language ?: $this->getDefaultLanguage(); - if ($langcode == $default_language->id) { - $langcode = Language::LANGCODE_DEFAULT; - } + if ($langcode != Language::LANGCODE_DEFAULT && $langcode == $this->language->id) { + $langcode = Language::LANGCODE_DEFAULT; } // Populate entity translation object cache so it will be available for all @@ -613,7 +641,7 @@ public function getTranslation($langcode) { // If the entity or the requested language is not a configured // language, we fall back to the entity itself, since in this case it // cannot have translations. - $translation = empty($this->getDefaultLanguage()->locked) && empty($languages[$langcode]->locked) ? $this->addTranslation($langcode) : $this; + $translation = empty($this->language->locked) && empty($languages[$langcode]->locked) ? $this->addTranslation($langcode) : $this; } } } @@ -674,8 +702,7 @@ protected function initializeTranslation($langcode) { * {@inheritdoc} */ public function hasTranslation($langcode) { - $default_language = $this->language ?: $this->getDefaultLanguage(); - if ($langcode == $default_language->id) { + if ($langcode == $this->language->id) { $langcode = Language::LANGCODE_DEFAULT; } return !empty($this->translations[$langcode]['status']); @@ -722,7 +749,7 @@ public function addTranslation($langcode, array $values = array()) { * {@inheritdoc} */ public function removeTranslation($langcode) { - if (isset($this->translations[$langcode]) && $langcode != Language::LANGCODE_DEFAULT && $langcode != $this->getDefaultLanguage()->id) { + if (isset($this->translations[$langcode]) && $langcode != Language::LANGCODE_DEFAULT && $langcode != $this->language->id) { foreach ($this->getPropertyDefinitions() as $name => $definition) { if (!empty($definition['translatable'])) { unset($this->values[$name][$langcode]); @@ -741,7 +768,7 @@ public function removeTranslation($langcode) { * {@inheritdoc} */ public function initTranslation($langcode) { - if ($langcode != Language::LANGCODE_DEFAULT && $langcode != $this->getDefaultLanguage()->id) { + if ($langcode != Language::LANGCODE_DEFAULT && $langcode != $this->language->id) { $this->translations[$langcode]['status'] = static::TRANSLATION_EXISTING; } } @@ -754,8 +781,7 @@ public function getTranslationLanguages($include_default = TRUE) { unset($translations[Language::LANGCODE_DEFAULT]); if ($include_default) { - $langcode = $this->getDefaultLanguage()->id; - $translations[$langcode] = TRUE; + $translations[$this->language->id] = TRUE; } // Now load language objects based upon translation langcodes. @@ -915,10 +941,7 @@ public function __clone() { // original reference and re-creating its values. $this->clearTranslationCache(); $translations = $this->translations; - unset($this->translations); - // This will trigger the magic setter as the translations array is - // undefined now. - $this->translations = $translations; + $this->translations = &$translations; } } diff --git a/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php index 5497efb..11666f3 100644 --- a/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php @@ -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); } @@ -849,41 +849,47 @@ protected function doLoadFieldItems($entities, $age) { } // Load field data. - $all_langcodes = array_keys(language_list()); + $all_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') ->condition('deleted', 0) - ->condition('langcode', $langcodes, 'IN') + ->condition('langcode', $all_langcodes, 'IN') ->orderBy('delta') ->execute(); + $translatable = $field->isFieldTranslatable(); $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. + // @todo Remove BC support for 'und' untranslatable fields as soon as + // can write a migration for them. + if ($translatable || $row->langcode == Language::LANGCODE_NOT_SPECIFIED || $row->langcode == $entities[$row->entity_id]->getUntranslated()->language()->id) { + 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 +903,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 +939,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); diff --git a/core/lib/Drupal/Core/Field/FieldItemList.php b/core/lib/Drupal/Core/Field/FieldItemList.php index 00dcaba..da50ce1 100644 --- a/core/lib/Drupal/Core/Field/FieldItemList.php +++ b/core/lib/Drupal/Core/Field/FieldItemList.php @@ -39,7 +39,7 @@ class FieldItemList extends ItemList implements FieldItemListInterface { * * @var string */ - protected $langcode = Language::LANGCODE_DEFAULT; + protected $langcode = Language::LANGCODE_NOT_SPECIFIED; /** * Overrides TypedData::__construct(). diff --git a/core/lib/Drupal/Core/Language/Language.php b/core/lib/Drupal/Core/Language/Language.php index b5ea710..0174a99 100644 --- a/core/lib/Drupal/Core/Language/Language.php +++ b/core/lib/Drupal/Core/Language/Language.php @@ -59,11 +59,8 @@ 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. */ - const LANGCODE_DEFAULT = 'und'; + const LANGCODE_DEFAULT = 'xx-default'; /** * The language state when referring to configurable languages. diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc index 6d02b9d..4c767f0 100644 --- a/core/modules/content_translation/content_translation.admin.inc +++ b/core/modules/content_translation/content_translation.admin.inc @@ -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'); - } -} - diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 66bb500..0d9c3cb 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -86,10 +86,6 @@ function content_translation_entity_info_alter(array &$entity_info) { $info['translation']['content_translation'] = array(); } - // 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 no menu base path is provided we default to the usual @@ -217,12 +213,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; } @@ -811,36 +801,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, + ); } /** diff --git a/core/modules/content_translation/content_translation.routing.yml b/core/modules/content_translation/content_translation.routing.yml deleted file mode 100644 index 51db4a3..0000000 --- a/core/modules/content_translation/content_translation.routing.yml +++ /dev/null @@ -1,6 +0,0 @@ -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' diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Form/TranslatableForm.php b/core/modules/content_translation/lib/Drupal/content_translation/Form/TranslatableForm.php deleted file mode 100644 index 363cb1c..0000000 --- a/core/modules/content_translation/lib/Drupal/content_translation/Form/TranslatableForm.php +++ /dev/null @@ -1,156 +0,0 @@ -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() ? "
" . t("All the existing translations of this field will be deleted.
This action cannot be undone.") : ''; - return $description; - } - - /** - * {@inheritdoc} - */ - public function getCancelRoute() { - return array( - 'route_name' => '', - ); - } - - /** - * {@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); - - $t_args = array('%field' => $this->fieldName); - $title = !$translatable ? t('Enabling translation for the %field field', $t_args) : t('Disabling translation for the %field field', $t_args); - - $batch = array( - 'title' => $title, - 'operations' => $operations, - 'finished' => 'content_translation_translatable_batch_done', - 'file' => drupal_get_path('module', 'content_translation') . '/content_translation.admin.inc', - ); - - batch_set($batch); - - } - -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php index bc0e195..eabcdeb 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -7,10 +7,13 @@ namespace Drupal\system\Tests\Entity; +use Drupal\Component\Utility\MapArray; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\FieldableDatabaseStorageController; use Drupal\Core\Language\Language; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\entity_test\Entity\EntityTestMulRev; -use Drupal\Component\Utility\MapArray; +use Drupal\field\Field as FieldService; /** * Tests entity translation. @@ -19,6 +22,20 @@ class EntityTranslationTest extends EntityUnitTestBase { protected $langcodes; + /** + * The test field name. + * + * @var string + */ + protected $field_name; + + /** + * The untranslatable test field name. + * + * @var string + */ + protected $untranslatable_field_name; + public static $modules = array('language', 'entity_test'); public static function getInfo() { @@ -54,7 +71,10 @@ function setUp() { // Create a translatable test field. $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); - // Create instance in all entity variations. + // Create an untranslatable test field. + $this->untranslatable_field_name = drupal_strtolower($this->randomName() . '_field_name'); + + // Create field instances in all entity variations. foreach (entity_test_entity_types() as $entity_type) { entity_create('field_entity', array( 'name' => $this->field_name, @@ -69,6 +89,19 @@ function setUp() { 'bundle' => $entity_type, ))->save(); $this->instance[$entity_type] = field_read_instance($entity_type, $this->field_name, $entity_type); + + entity_create('field_entity', array( + 'name' => $this->untranslatable_field_name, + 'entity_type' => $entity_type, + 'type' => 'text', + 'cardinality' => 4, + 'translatable' => FALSE, + ))->save(); + entity_create('field_instance', array( + 'field_name' => $this->untranslatable_field_name, + 'entity_type' => $entity_type, + 'bundle' => $entity_type, + ))->save(); } // Create the default languages. @@ -120,7 +153,7 @@ protected function _testEntityLanguageMethods($entity_type) { // Get the value. $field = $entity->getTranslation(Language::LANGCODE_DEFAULT)->get($this->field_name); $this->assertEqual($field->value, 'default value', format_string('%entity_type: Untranslated value retrieved.', array('%entity_type' => $entity_type))); - $this->assertEqual($field->getLangcode(), Language::LANGCODE_DEFAULT, format_string('%entity_type: Field object has the expected langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($field->getLangcode(), Language::LANGCODE_NOT_SPECIFIED, format_string('%entity_type: Field object has the expected langcode.', array('%entity_type' => $entity_type))); // Set the value in a certain language. As the entity is not // language-specific it should use the default language and so ignore the @@ -133,11 +166,12 @@ protected function _testEntityLanguageMethods($entity_type) { // language-specific entity. $field = $entity->getTranslation($this->langcodes[1])->get($this->field_name); $this->assertEqual($field->value, 'default value2', format_string('%entity_type: Untranslated value retrieved.', array('%entity_type' => $entity_type))); - $this->assertEqual($field->getLangcode(), Language::LANGCODE_DEFAULT, format_string('%entity_type: Field object has the expected langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($field->getLangcode(), Language::LANGCODE_NOT_SPECIFIED, format_string('%entity_type: Field object has the expected langcode.', array('%entity_type' => $entity_type))); // Now, make the entity language-specific by assigning a language and test // translating it. - $entity->langcode->value = $this->langcodes[0]; + $default_langcode = $this->langcodes[0]; + $entity->langcode->value = $default_langcode; $entity->{$this->field_name} = array(); $this->assertEqual($entity->language(), language_load($this->langcodes[0]), format_string('%entity_type: Entity language retrieved.', array('%entity_type' => $entity_type))); $this->assertFalse($entity->getTranslationLanguages(FALSE), format_string('%entity_type: No translations are available', array('%entity_type' => $entity_type))); @@ -147,7 +181,7 @@ protected function _testEntityLanguageMethods($entity_type) { // Get the value. $field = $entity->get($this->field_name); $this->assertEqual($field->value, 'default value', format_string('%entity_type: Untranslated value retrieved.', array('%entity_type' => $entity_type))); - $this->assertEqual($field->getLangcode(), Language::LANGCODE_DEFAULT, format_string('%entity_type: Field object has the expected langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($field->getLangcode(), $default_langcode, format_string('%entity_type: Field object has the expected langcode.', array('%entity_type' => $entity_type))); // Set a translation. $entity->getTranslation($this->langcodes[1])->set($this->field_name, array(0 => array('value' => 'translation 1'))); @@ -158,7 +192,7 @@ protected function _testEntityLanguageMethods($entity_type) { // Make sure the untranslated value stays. $field = $entity->get($this->field_name); $this->assertEqual($field->value, 'default value', 'Untranslated value stays.'); - $this->assertEqual($field->getLangcode(), Language::LANGCODE_DEFAULT, 'Untranslated value has the expected langcode.'); + $this->assertEqual($field->getLangcode(), $default_langcode, 'Untranslated value has the expected langcode.'); $translations[$this->langcodes[1]] = language_load($this->langcodes[1]); $this->assertEqual($entity->getTranslationLanguages(FALSE), $translations, 'Translations retrieved.'); @@ -190,7 +224,7 @@ protected function _testEntityLanguageMethods($entity_type) { // Get the value. $field = $entity->get($field_name); $this->assertEqual($field->value, 'default value2', format_string('%entity_type: Untranslated value set into a translation in non-strict mode.', array('%entity_type' => $entity_type))); - $this->assertEqual($field->getLangcode(), Language::LANGCODE_DEFAULT, format_string('%entity_type: Field object has the expected langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($field->getLangcode(), $default_langcode, format_string('%entity_type: Field object has the expected langcode.', array('%entity_type' => $entity_type))); } /** @@ -219,20 +253,19 @@ protected function _testMultilingualProperties($entity_type) { $entity = entity_create($entity_type, array('name' => $name, 'user_id' => $uid)); $entity->save(); $entity = entity_load($entity_type, $entity->id()); - $this->assertEqual($entity->language()->id, Language::LANGCODE_NOT_SPECIFIED, format_string('%entity_type: Entity created as language neutral.', array('%entity_type' => $entity_type))); + $default_langcode = $entity->language()->id; + $this->assertEqual($default_langcode, Language::LANGCODE_NOT_SPECIFIED, format_string('%entity_type: Entity created as language neutral.', array('%entity_type' => $entity_type))); $field = $entity->getTranslation(Language::LANGCODE_DEFAULT)->get('name'); $this->assertEqual($name, $field->value, format_string('%entity_type: The entity name has been correctly stored as language neutral.', array('%entity_type' => $entity_type))); - $this->assertEqual(Language::LANGCODE_DEFAULT, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($default_langcode, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); $this->assertEqual($uid, $entity->getTranslation(Language::LANGCODE_DEFAULT)->get('user_id')->target_id, format_string('%entity_type: The entity author has been correctly stored as language neutral.', array('%entity_type' => $entity_type))); - // As fields, translatable properties should ignore the given langcode and - // use neutral language if the entity is not translatable. $field = $entity->getTranslation($langcode)->get('name'); $this->assertEqual($name, $field->value, format_string('%entity_type: The entity name defaults to neutral language.', array('%entity_type' => $entity_type))); - $this->assertEqual(Language::LANGCODE_DEFAULT, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($default_langcode, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); $this->assertEqual($uid, $entity->getTranslation($langcode)->get('user_id')->target_id, format_string('%entity_type: The entity author defaults to neutral language.', array('%entity_type' => $entity_type))); $field = $entity->get('name'); $this->assertEqual($name, $field->value, format_string('%entity_type: The entity name can be retrieved without specifying a language.', array('%entity_type' => $entity_type))); - $this->assertEqual(Language::LANGCODE_DEFAULT, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($default_langcode, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); $this->assertEqual($uid, $entity->get('user_id')->target_id, format_string('%entity_type: The entity author can be retrieved without specifying a language.', array('%entity_type' => $entity_type))); // Create a language-aware entity and check that properties are stored @@ -240,20 +273,21 @@ protected function _testMultilingualProperties($entity_type) { $entity = entity_create($entity_type, array('name' => $name, 'user_id' => $uid, 'langcode' => $langcode)); $entity->save(); $entity = entity_load($entity_type, $entity->id()); - $this->assertEqual($entity->language()->id, $langcode, format_string('%entity_type: Entity created as language specific.', array('%entity_type' => $entity_type))); + $default_langcode = $entity->language()->id; + $this->assertEqual($default_langcode, $langcode, format_string('%entity_type: Entity created as language specific.', array('%entity_type' => $entity_type))); $field = $entity->getTranslation($langcode)->get('name'); $this->assertEqual($name, $field->value, format_string('%entity_type: The entity name has been correctly stored as a language-aware property.', array('%entity_type' => $entity_type))); - $this->assertEqual(Language::LANGCODE_NOT_SPECIFIED, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($default_langcode, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); $this->assertEqual($uid, $entity->getTranslation($langcode)->get('user_id')->target_id, format_string('%entity_type: The entity author has been correctly stored as a language-aware property.', array('%entity_type' => $entity_type))); // Translatable properties on a translatable entity should use default // language if Language::LANGCODE_NOT_SPECIFIED is passed. $field = $entity->getTranslation(Language::LANGCODE_NOT_SPECIFIED)->get('name'); $this->assertEqual($name, $field->value, format_string('%entity_type: The entity name defaults to the default language.', array('%entity_type' => $entity_type))); - $this->assertEqual(Language::LANGCODE_NOT_SPECIFIED, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($default_langcode, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); $this->assertEqual($uid, $entity->getTranslation(Language::LANGCODE_NOT_SPECIFIED)->get('user_id')->target_id, format_string('%entity_type: The entity author defaults to the default language.', array('%entity_type' => $entity_type))); $field = $entity->get('name'); $this->assertEqual($name, $field->value, format_string('%entity_type: The entity name can be retrieved without specifying a language.', array('%entity_type' => $entity_type))); - $this->assertEqual(Language::LANGCODE_NOT_SPECIFIED, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); + $this->assertEqual($default_langcode, $field->getLangcode(), format_string('%entity_type: The field object has the expect langcode.', array('%entity_type' => $entity_type))); $this->assertEqual($uid, $entity->get('user_id')->target_id, format_string('%entity_type: The entity author can be retrieved without specifying a language.', array('%entity_type' => $entity_type))); // Create property translations. @@ -285,8 +319,7 @@ protected function _testMultilingualProperties($entity_type) { ); $field = $entity->getTranslation($langcode)->get('name'); $this->assertEqual($properties[$langcode]['name'][0], $field->value, format_string('%entity_type: The entity name has been correctly stored for language %langcode.', $args)); - // Fields for the default entity langcode are seen as language neutral. - $field_langcode = ($langcode == $entity->language()->id) ? Language::LANGCODE_NOT_SPECIFIED : $langcode; + $field_langcode = ($langcode == $entity->language()->id) ? $default_langcode : $langcode; $this->assertEqual($field_langcode, $field->getLangcode(), format_string('%entity_type: The field object has the expected langcode %langcode.', $args)); $this->assertEqual($properties[$langcode]['user_id'][0], $entity->getTranslation($langcode)->get('user_id')->target_id, format_string('%entity_type: The entity author has been correctly stored for language %langcode.', $args)); } @@ -482,6 +515,9 @@ function testEntityTranslationAPI() { $cloned = clone $entity; $translation = $cloned->getTranslation($langcode); $this->assertNotIdentical($entity, $translation->getUntranslated(), 'A cloned entity object has no reference to the original one.'); + $entity->removeTranslation($langcode); + $this->assertFalse($entity->hasTranslation($langcode)); + $this->assertTrue($cloned->hasTranslation($langcode)); // Check that per-language defaults are properly populated. $entity = $this->reloadEntity($entity); @@ -596,4 +632,152 @@ function testFieldDefinitions() { } } + /** + * Tests that changing entity language does not break field language. + */ + public function testLanguageChange() { + $entity_type = 'entity_test_mul'; + $controller = $this->entityManager->getStorageController($entity_type); + $langcode = $this->langcodes[0]; + + // check that field languages match entity language regardless of field + // translatability. + $values = array( + 'langcode' => $langcode, + $this->field_name => $this->randomName(), + $this->untranslatable_field_name => $this->randomName(), + ); + $entity = $controller->create($values); + foreach (array($this->field_name, $this->untranslatable_field_name) as $field_name) { + $this->assertEqual($entity->get($field_name)->getLangcode(), $langcode, 'Field language works as expected.'); + } + + // Check that field languages keep matching entity language even after + // changing it. + $langcode = $this->langcodes[1]; + $entity->langcode->value = $langcode; + foreach (array($this->field_name, $this->untranslatable_field_name) as $field_name) { + $this->assertEqual($entity->get($field_name)->getLangcode(), $langcode, 'Field language works as expected after changing entity language.'); + } + + // Check that entity translation does not affect the language of original + // field values and untranslatable ones. + $langcode = $this->langcodes[0]; + $entity->addTranslation($this->langcodes[2], array($this->field_name => $this->randomName())); + $entity->langcode->value = $langcode; + foreach (array($this->field_name, $this->untranslatable_field_name) as $field_name) { + $this->assertEqual($entity->get($field_name)->getLangcode(), $langcode, 'Field language works as expected after translating the entity and changing language.'); + } + } + + /** + * Tests field SQL storage. + */ + public function testFieldSqlStorage() { + $entity_type = 'entity_test_mul'; + + $controller = $this->entityManager->getStorageController($entity_type); + $values = array( + $this->field_name => $this->randomName(), + $this->untranslatable_field_name => $this->randomName(), + ); + $entity = $controller->create($values); + $entity->save(); + + // Tests that when changing language field language codes are still correct. + $langcode = $this->langcodes[0]; + $entity->langcode->value = $langcode; + $entity->save(); + $this->assertFieldStorageLangcode($entity, 'Field language successfully changed from language neutral.'); + $langcode = $this->langcodes[1]; + $entity->langcode->value = $langcode; + $entity->save(); + $this->assertFieldStorageLangcode($entity, 'Field language successfully changed.'); + $langcode = Language::LANGCODE_NOT_SPECIFIED; + $entity->langcode->value = $langcode; + $entity->save(); + $this->assertFieldStorageLangcode($entity, 'Field language successfully changed to language neutral.'); + + // Test that after switching field translatability things keep working as + // before. + $this->toggleFieldTranslatability($entity_type); + $entity = $this->reloadEntity($entity); + foreach (array($this->field_name, $this->untranslatable_field_name) as $field_name) { + $this->assertEqual($entity->get($field_name)->value, $values[$field_name], 'Field language works as expected after switching translatability.'); + } + + // Test that after disabling field translatability translated values are not + // loaded. + $this->toggleFieldTranslatability($entity_type); + $entity = $this->reloadEntity($entity); + $entity->langcode->value = $this->langcodes[0]; + $translation = $entity->addTranslation($this->langcodes[1]); + $translated_value = $this->randomName(); + $translation->get($this->field_name)->value = $translated_value; + $translation->save(); + $this->toggleFieldTranslatability($entity_type); + $entity = $this->reloadEntity($entity); + $this->assertEqual($entity->getTranslation($this->langcodes[1])->get($this->field_name)->value, $values[$this->field_name], 'Existing field translations are not loaded for untranslatable fields.'); + } + + /** + * Checks whether field languages are correctly stored for the given entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity fields are attached to. + * @param string $message + * (optional) A message to display with the assertion. + */ + protected function assertFieldStorageLangcode(ContentEntityInterface $entity, $message = '') { + $status = TRUE; + $entity_type = $entity->entityType(); + $id = $entity->id(); + $langcode = $entity->getUntranslated()->language()->id; + $fields = array($this->field_name, $this->untranslatable_field_name); + + foreach ($fields as $field_name) { + $field = FieldService::fieldInfo()->getField($entity_type, $field_name); + $tables = array( + FieldableDatabaseStorageController::_fieldTableName($field), + FieldableDatabaseStorageController::_fieldRevisionTableName($field), + ); + + foreach ($tables as $table) { + $record = \Drupal::database() + ->select($table, 'f') + ->fields('f') + ->condition('f.entity_id', $id) + ->condition('f.revision_id', $id) + ->execute() + ->fetchObject(); + + if ($record->langcode != $langcode) { + $status = FALSE; + break; + } + } + } + + return $this->assertTrue($status, $message); + } + + /** + * Toggles field translatability. + * + * @param string $entity_type + * The type of the entity fields are attached to. + */ + protected function toggleFieldTranslatability($entity_type) { + $fields = array($this->field_name, $this->untranslatable_field_name); + foreach ($fields as $field_name) { + $field = FieldService::fieldInfo()->getField($entity_type, $field_name); + $translatable = !$field->isFieldTranslatable(); + $field->set('translatable', $translatable); + $field->save(); + FieldService::fieldInfo()->flush(); + $field = FieldService::fieldInfo()->getField($entity_type, $field_name); + $this->assertEqual($field->isFieldTranslatable(), $translatable, 'Field translatability changed.'); + } + \Drupal::cache('field')->deleteAll(); + } } diff --git a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php index 0b47055..49d9f78 100644 --- a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php +++ b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php @@ -128,7 +128,7 @@ function testProcessedCache() { $entity = entity_load($entity_type, $entity->id()); $cache = cache('field')->get("field:$entity_type:" . $entity->id()); $this->assertEqual($cache->data, array( - Language::LANGCODE_DEFAULT => array( + Language::LANGCODE_NOT_SPECIFIED => array( 'summary_field' => array( 0 => array( 'value' => $value, @@ -144,7 +144,7 @@ function testProcessedCache() { // Inject fake processed values into the cache to make sure that these are // used as-is and not re-calculated when the entity is loaded. $data = array( - Language::LANGCODE_DEFAULT => array( + Language::LANGCODE_NOT_SPECIFIED => array( 'summary_field' => array( 0 => array( 'value' => $value,