Commit aeecb0a4 authored by webchick's avatar webchick
Browse files

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
......@@ -61,20 +61,6 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
*/
protected $fields = array();
/**
* Local cache for the entity language.
*
* @var \Drupal\Core\Language\Language
*/
protected $language;
/**
* Local cache for the available language objects.
*
* @var array
*/
protected $languages;
/**
* Local cache for field definitions.
*
......@@ -91,6 +77,13 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
*/
protected $uriPlaceholderReplacements;
/**
* Local cache for the available language objects.
*
* @var array
*/
protected $languages;
/**
* Language code identifying the entity active language.
*
......@@ -101,6 +94,13 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
*/
protected $activeLangcode = Language::LANGCODE_DEFAULT;
/**
* Local cache for the default language code.
*
* @var string
*/
protected $defaultLangcode;
/**
* An array of entity translation metadata.
*
......@@ -150,14 +150,14 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans
$this->values[$key] = $value;
}
// Initialize translations. Ensure we have at least an entry for the entity
// original language.
// Initialize translations. Ensure we have at least an entry for the default
// language.
$data = array('status' => static::TRANSLATION_EXISTING);
$this->translations[Language::LANGCODE_DEFAULT] = $data;
$this->setDefaultLangcode();
if ($translations) {
$default_langcode = $this->language()->id;
foreach ($translations as $langcode) {
if ($langcode != $default_langcode && $langcode != Language::LANGCODE_DEFAULT) {
if ($langcode != $this->defaultLangcode && $langcode != Language::LANGCODE_DEFAULT) {
$this->translations[$langcode] = $data;
}
}
......@@ -385,50 +385,56 @@ public function get($property_name) {
*
* @return \Drupal\Core\Field\FieldItemListInterface
*/
protected function getTranslatedField($property_name, $langcode) {
protected function getTranslatedField($name, $langcode) {
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
$message = 'The entity object refers to a removed translation (@langcode) and cannot be manipulated.';
throw new \InvalidArgumentException(format_string($message, array('@langcode' => $this->activeLangcode)));
}
// Populate $this->fields to speed-up further look-ups and to keep track of
// fields objects, possibly holding changes to field values.
if (!isset($this->fields[$property_name][$langcode])) {
$definition = $this->getPropertyDefinition($property_name);
if (!isset($this->fields[$name][$langcode])) {
$definition = $this->getPropertyDefinition($name);
if (!$definition) {
throw new \InvalidArgumentException('Field ' . check_plain($property_name) . ' is unknown.');
throw new \InvalidArgumentException('Field ' . check_plain($name) . ' is unknown.');
}
// Non-translatable fields are always stored with
// Language::LANGCODE_DEFAULT as key.
if ($langcode != Language::LANGCODE_DEFAULT && !$definition->isFieldTranslatable()) {
if (!isset($this->fields[$property_name][Language::LANGCODE_DEFAULT])) {
$this->fields[$property_name][Language::LANGCODE_DEFAULT] = $this->getTranslatedField($property_name, Language::LANGCODE_DEFAULT);
$default = $langcode == Language::LANGCODE_DEFAULT;
if (!$default && !$definition->isFieldTranslatable()) {
if (!isset($this->fields[$name][Language::LANGCODE_DEFAULT])) {
$this->fields[$name][Language::LANGCODE_DEFAULT] = $this->getTranslatedField($name, Language::LANGCODE_DEFAULT);
}
$this->fields[$property_name][$langcode] = &$this->fields[$property_name][Language::LANGCODE_DEFAULT];
$this->fields[$name][$langcode] = &$this->fields[$name][Language::LANGCODE_DEFAULT];
}
else {
$value = NULL;
if (isset($this->values[$property_name][$langcode])) {
$value = $this->values[$property_name][$langcode];
if (isset($this->values[$name][$langcode])) {
$value = $this->values[$name][$langcode];
}
$field = \Drupal::typedData()->getPropertyInstance($this, $name, $value);
if ($default) {
// $this->defaultLangcode might not be set if we are initializing the
// default language code cache, in which case there is no valid
// langcode to assign.
$field_langcode = isset($this->defaultLangcode) ? $this->defaultLangcode : Language::LANGCODE_NOT_SPECIFIED;
}
$field = \Drupal::typedData()->getPropertyInstance($this, $property_name, $value);
$field->setLangcode($langcode);
$this->fields[$property_name][$langcode] = $field;
else {
$field_langcode = $langcode;
}
$field->setLangcode($field_langcode);
$this->fields[$name][$langcode] = $field;
}
}
return $this->fields[$property_name][$langcode];
return $this->fields[$name][$langcode];
}
/**
* {@inheritdoc}
*/
public function set($property_name, $value, $notify = TRUE) {
$this->get($property_name)->setValue($value, FALSE);
if ($property_name == 'langcode') {
// Avoid using unset as this unnecessarily triggers magic methods later
// on.
$this->language = NULL;
}
public function set($name, $value, $notify = TRUE) {
// If default language changes we need to react to that.
$notify = $name == 'langcode';
$this->get($name)->setValue($value, $notify);
}
/**
......@@ -530,47 +536,63 @@ public function access($operation = 'view', AccountInterface $account = NULL) {
* {@inheritdoc}
*/
public function language() {
$language = NULL;
if ($this->activeLangcode != Language::LANGCODE_DEFAULT) {
if (!isset($this->languages[$this->activeLangcode])) {
$this->languages += language_list(Language::STATE_ALL);
}
return $this->languages[$this->activeLangcode];
$language = $this->languages[$this->activeLangcode];
}
else {
return $this->language ?: $this->getDefaultLanguage();
$language = $this->languages[$this->defaultLangcode];
}
return $language;
}
/**
* Returns the entity original language.
* Populates the local cache for the default language code.
*/
protected function setDefaultLangcode() {
// Get the language code if the property exists.
if ($this->getPropertyDefinition('langcode') && ($item = $this->get('langcode')) && isset($item->language)) {
$this->defaultLangcode = $item->language->id;
}
if (empty($this->defaultLangcode)) {
// Make sure we return a proper language object.
$this->defaultLangcode = Language::LANGCODE_NOT_SPECIFIED;
}
// 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->defaultLangcode);
}
}
/**
* Updates language for already instantiated fields.
*
* @return \Drupal\Core\Language\Language
* A language object.
*/
protected function getDefaultLanguage() {
// Keep a local cache of the language object and clear it if the langcode
// gets changed, see ContentEntityBase::onChange().
if (!isset($this->language)) {
// Get the language code if the property exists.
if ($this->getPropertyDefinition('langcode') && ($item = $this->get('langcode')) && isset($item->language)) {
$this->language = $item->language;
}
if (empty($this->language)) {
// Make sure we return a proper language object.
$this->language = new Language(array('id' => Language::LANGCODE_NOT_SPECIFIED, 'locked' => TRUE));
protected function updateFieldLangcodes($langcode) {
foreach ($this->fields as $name => $items) {
if (!empty($items[Language::LANGCODE_DEFAULT])) {
$items[Language::LANGCODE_DEFAULT]->setLangcode($langcode);
}
}
return $this->language;
}
/**
* {@inheritdoc}
*/
public function onChange($property_name) {
if ($property_name == 'langcode') {
// Avoid using unset as this unnecessarily triggers magic methods later
// on.
$this->language = NULL;
public function onChange($name) {
if ($name == 'langcode') {
$this->setDefaultLangcode();
if (isset($this->translations[$this->defaultLangcode])) {
$message = format_string('A translation already exists for the specified language (@langcode).', array('@langcode' => $this->defaultLangcode));
throw new \InvalidArgumentException($message);
}
$this->updateFieldLangcodes($this->defaultLangcode);
}
}
......@@ -582,11 +604,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->defaultLangcode) {
$langcode = Language::LANGCODE_DEFAULT;
}
// Populate entity translation object cache so it will be available for all
......@@ -608,12 +627,11 @@ public function getTranslation($langcode) {
else {
// If we were given a valid language and there is no translation for it,
// we return a new one.
$languages = language_list(Language::STATE_ALL);
if (isset($languages[$langcode])) {
if (isset($this->languages[$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->languages[$this->defaultLangcode]->locked) && empty($this->languages[$langcode]->locked) ? $this->addTranslation($langcode) : $this;
}
}
}
......@@ -674,8 +692,7 @@ protected function initializeTranslation($langcode) {
* {@inheritdoc}
*/
public function hasTranslation($langcode) {
$default_language = $this->language ?: $this->getDefaultLanguage();
if ($langcode == $default_language->id) {
if ($langcode == $this->defaultLangcode) {
$langcode = Language::LANGCODE_DEFAULT;
}
return !empty($this->translations[$langcode]['status']);
......@@ -685,8 +702,7 @@ public function hasTranslation($langcode) {
* {@inheritdoc}
*/
public function addTranslation($langcode, array $values = array()) {
$languages = language_list(Language::STATE_ALL);
if (!isset($languages[$langcode]) || $this->hasTranslation($langcode)) {
if (!isset($this->languages[$langcode]) || $this->hasTranslation($langcode)) {
$message = 'Invalid translation language (@langcode) specified.';
throw new \InvalidArgumentException(format_string($message, array('@langcode' => $langcode)));
}
......@@ -722,7 +738,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->defaultLangcode) {
foreach ($this->getPropertyDefinitions() as $name => $definition) {
if ($definition->isFieldTranslatable()) {
unset($this->values[$name][$langcode]);
......@@ -741,7 +757,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->defaultLangcode) {
$this->translations[$langcode]['status'] = static::TRANSLATION_EXISTING;
}
}
......@@ -754,12 +770,11 @@ public function getTranslationLanguages($include_default = TRUE) {
unset($translations[Language::LANGCODE_DEFAULT]);
if ($include_default) {
$langcode = $this->getDefaultLanguage()->id;
$translations[$langcode] = TRUE;
$translations[$this->defaultLangcode] = TRUE;
}
// Now load language objects based upon translation langcodes.
return array_intersect_key(language_list(Language::STATE_ALL), $translations);
return array_intersect_key($this->languages, $translations);
}
/**
......@@ -920,14 +935,11 @@ public function __clone() {
}
}
// Ensure the translations array is actually cloned by removing the
// original reference and re-creating its values.
// Ensure the translations array is actually cloned by overwriting the
// original reference with one pointing to a copy of the array.
$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;
}
}
......
......@@ -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