diff --git a/core/core.services.yml b/core/core.services.yml index 6f8cce6..58611e2 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -194,6 +194,9 @@ services: language_manager: class: Drupal\Core\Language\LanguageManager arguments: ['@state'] + language_fallback_manager: + class: Drupal\Core\Language\NoFallbackManager + arguments: ['@language_manager'] string_translator.custom_strings: class: Drupal\Core\StringTranslation\Translator\CustomStrings tags: diff --git a/core/includes/language.inc b/core/includes/language.inc index e63acaa..eef7202 100644 --- a/core/includes/language.inc +++ b/core/includes/language.inc @@ -542,20 +542,14 @@ function language_url_split_prefix($path, $languages) { * * @return * An array of language codes. + * + * @see \Drupal\Core\Language\FallbackManagerInterface::getCandidates() + * + * @deprecated This has been deprectaed in favor of the language fallback + * manager. */ function language_fallback_get_candidates($type = Language::TYPE_CONTENT) { - $fallback_candidates = &drupal_static(__FUNCTION__); - - if (!isset($fallback_candidates)) { - // Get languages ordered by weight, add Language::LANGCODE_NOT_SPECIFIED at the end. - $fallback_candidates = array_keys(language_list()); - $fallback_candidates[] = Language::LANGCODE_NOT_SPECIFIED; - - // Let other modules hook in and add/change candidates. - drupal_alter('language_fallback_candidates', $fallback_candidates); - } - - return $fallback_candidates; + return Drupal::service('language_fallback_manager')->getCandidates(array('data' => $type)); } /** diff --git a/core/includes/schema.inc b/core/includes/schema.inc index ac0ffad..e7ec164 100644 --- a/core/includes/schema.inc +++ b/core/includes/schema.inc @@ -525,14 +525,17 @@ function drupal_write_record($table, &$record, $primary_keys = array()) { * The converted value. */ function drupal_schema_get_field_value(array $info, $value) { - if ($info['type'] == 'int' || $info['type'] == 'serial') { - $value = (int) $value; - } - elseif ($info['type'] == 'float') { - $value = (float) $value; - } - else { - $value = (string) $value; + // Preserve legal NULL values. + if (isset($value) || !empty($info['not null'])) { + if ($info['type'] == 'int' || $info['type'] == 'serial') { + $value = (int) $value; + } + elseif ($info['type'] == 'float') { + $value = (float) $value; + } + else { + $value = (string) $value; + } } return $value; } diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index 5eeb637..df7d0fc 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -7,7 +7,9 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use PDO; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Entity\Query\QueryInterface; @@ -59,13 +61,29 @@ class DatabaseStorageController extends EntityStorageControllerBase { protected $database; /** + * The language manager service. + * + * @var \Drupal\Core\Language\LanguageManager + */ + protected $languageManager; + + /** + * The language fallback manager service. + * + * @var \Drupal\Core\Language\FallbackManagerInterface; + */ + protected $languageFallbackManager; + + /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { return new static( $entity_type, $entity_info, - $container->get('database') + $container->get('database'), + $container->get('language_manager'), + $container->get('language_fallback_manager') ); } @@ -78,11 +96,15 @@ public static function createInstance(ContainerInterface $container, $entity_typ * An array of entity info for the entity type. * @param \Drupal\Core\Database\Connection $database * The database connection to be used. + * @param \Drupal\Core\Language\FallbackManagerInterface + * The language fallback manager. */ - public function __construct($entity_type, array $entity_info, Connection $database) { + public function __construct($entity_type, array $entity_info, Connection $database, LanguageManager $language_manager, FallbackManagerInterface $fallback_manager) { parent::__construct($entity_type, $entity_info); $this->database = $database; + $this->languageManager = $language_manager; + $this->languageFallbackManager = $fallback_manager; // Check if the entity type supports IDs. if (isset($this->entityInfo['entity_keys']['id'])) { @@ -367,6 +389,8 @@ public function create(array $values) { $entity_class::preCreate($this, $values); $entity = new $entity_class($values, $this->entityType); + $entity->setLanguageManager($this->languageManager); + $entity->setLanguageFallbackManager($this->languageFallbackManager); // Assign a new UUID if there is none yet. if ($this->uuidKey && !isset($entity->{$this->uuidKey})) { diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index 143a2ab..a4a84cb 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -7,7 +7,9 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use PDO; use Drupal\Core\Entity\Query\QueryInterface; @@ -53,8 +55,9 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { /** * Overrides DatabaseStorageController::__construct(). */ - public function __construct($entity_type, array $entity_info, Connection $database) { - parent::__construct($entity_type,$entity_info, $database); + public function __construct($entity_type, array $entity_info, Connection $database, LanguageManager $languageManager, FallbackManagerInterface $fallback_manager) { + parent::__construct($entity_type, $entity_info, $database, $languageManager, $fallback_manager); + $this->bundleKey = !empty($this->entityInfo['entity_keys']['bundle']) ? $this->entityInfo['entity_keys']['bundle'] : FALSE; $this->entityClass = $this->entityInfo['class']; @@ -109,6 +112,8 @@ public function create(array $values) { $bundle = $values[$this->bundleKey]; } $entity = new $this->entityClass(array(), $this->entityType, $bundle); + $entity->setLanguageManager($this->languageManager); + $entity->setLanguageFallbackManager($this->languageFallbackManager); foreach ($entity as $name => $field) { if (isset($values[$name])) { @@ -248,6 +253,8 @@ protected function mapFromStorageRecords(array $records, $load_revision = FALSE) $bundle = $this->bundleKey ? $record->{$this->bundleKey} : FALSE; // Turn the record into an entity class. $entities[$id] = new $this->entityClass($entities[$id], $this->entityType, $bundle); + $entities[$id]->setLanguageManager($this->languageManager); + $entities[$id]->setLanguageFallbackManager($this->languageFallbackManager); } } $this->attachPropertyData($entities, $load_revision); @@ -319,6 +326,8 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) { $bundle = $this->bundleKey ? $values[$this->bundleKey][Language::LANGCODE_DEFAULT] : FALSE; // Turn the record into an entity class. $entities[$id] = new $this->entityClass($values, $this->entityType, $bundle, array_keys($translations[$id])); + $entities[$id]->setLanguageManager($this->languageManager); + $entities[$id]->setLanguageFallbackManager($this->languageFallbackManager); } } } diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index ec05c1a..1c7941a 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -8,7 +8,9 @@ namespace Drupal\Core\Entity; use Drupal\Component\Uuid\Uuid; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\TypedData\TypedDataInterface; use IteratorAggregate; @@ -60,6 +62,22 @@ class Entity implements IteratorAggregate, EntityInterface { protected $isDefaultRevision = TRUE; /** + * The language manager to be used to retrieve languages and retrieve the + * current content language. + * + * @var \Drupal\Core\Language\LanguageManager + */ + protected $languageManager; + + /** + * The language fallback manager to be used to determine the current + * translation and field values fallback. + * + * @var \Drupal\Core\Language\FallbackManagerInterface + */ + protected $languageFallbackManager; + + /** * Constructs an Entity object. * * @param array $values @@ -120,6 +138,20 @@ public function setNewRevision($value = TRUE) { } /** + * {@inheritdoc} + */ + public function setLanguageManager(LanguageManager $language_manager) { + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function setLanguageFallbackManager(FallbackManagerInterface $fallback_manager) { + $this->languageFallbackManager = $fallback_manager; + } + + /** * Implements \Drupal\Core\Entity\EntityInterface::entityType(). */ public function entityType() { @@ -309,6 +341,17 @@ public function getTranslation($langcode) { } /** + * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation(). + * + * @return \Drupal\Core\Entity\EntityInterface + */ + public function getCurrentTranslation($langcode = NULL, $context = array()) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + return $this; + } + + /** * Returns the languages the entity is translated to. * * @todo: Remove once all entity types implement the entity field API. @@ -642,4 +685,14 @@ public function initTranslation($langcode) { // http://drupal.org/node/2004244 } + /** + * {@inheritdoc} + */ + public function applyLanguageFallback($context = array()) { + // @todo Config entities do not support entity translation hence we need to + // move the TranslatableInterface implementation to EntityNG. See + // http://drupal.org/node/2004244 + return $this; + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php index a3ffcb9..b622969 100644 --- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php +++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php @@ -7,7 +7,9 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use IteratorAggregate; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\TypedData\TypedDataInterface; @@ -269,6 +271,20 @@ public function setPropertyValues($values) { } /** + * {@inheritdoc} + */ + public function setLanguageManager(LanguageManager $language_manager) { + $this->decorated->setLanguageManager($language_manager); + } + + /** + * {@inheritdoc} + */ + public function setLanguageFallbackManager(FallbackManagerInterface $fallback_manager) { + $this->decorated->setLanguageFallbackManager($fallback_manager); + } + + /** * Forwards the call to the decorated entity. */ public function getPropertyDefinition($name) { @@ -430,6 +446,13 @@ public function getTranslation($langcode) { } /** + * {@inheritdoc} + */ + public function getCurrentTranslation($langcode = NULL, $context = array()) { + return $this->decorated->getCurrentTranslation($langcode = NULL, $context = array()); + } + + /** * Forwards the call to the decorated entity. */ public function getType() { @@ -626,4 +649,11 @@ public function initTranslation($langcode) { $this->decorated->initTranslation($langcode); } + /** + * {@inheritdoc} + */ + public function applyLanguageFallback($context = array()) { + return $this->decorated->applyLanguageFallback($context); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index ceb09de..13f613d 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -396,27 +396,11 @@ public function delete(array $form, array &$form_state) { /** * Implements \Drupal\Core\Entity\EntityFormControllerInterface::getFormLangcode(). */ - public function getFormLangcode(array $form_state) { - $entity = $this->entity; - - if (!empty($form_state['langcode'])) { - $langcode = $form_state['langcode']; + public function getFormLangcode(array &$form_state) { + if (empty($form_state['langcode'])) { + $form_state['langcode'] = $this->entity->getCurrentTranslation()->language()->id; } - else { - // If no form langcode was provided we default to the current content - // language and inspect existing translations to find a valid fallback, - // if any. - $translations = $entity->getTranslationLanguages(); - $langcode = language(Language::TYPE_CONTENT)->id; - $fallback = language_multilingual() ? language_fallback_get_candidates() : array(); - while (!empty($langcode) && !isset($translations[$langcode])) { - $langcode = array_shift($fallback); - } - } - - // If the site is not multilingual or no translation for the given form - // language is available, fall back to the entity language. - return !empty($langcode) ? $langcode : $entity->getUntranslated()->language()->id; + return $form_state['langcode']; } /** @@ -487,6 +471,8 @@ public function buildEntity(array $form, array &$form_state) { $form_state['controller'] = $this; // @todo Move entity_form_submit_build_entity() here. // @todo Exploit the Field API to process the submitted entity field. + // Make sure entity builders deal with the updated form language. + $this->updateFormLangcode($form_state); entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state, array('langcode' => $this->getFormLangcode($form_state))); return $entity; } diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php index d52363c..714270d 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php @@ -24,7 +24,7 @@ * @return string * The form language code. */ - public function getFormLangcode(array $form_state); + public function getFormLangcode(array &$form_state); /** * Checks whether the current form language matches the entity one. diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php index f760057..9e5bbe6 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php @@ -68,6 +68,9 @@ public function buildEntity(array $form, array &$form_state) { } } + // Make sure entity builders deal with the updated form language. + $this->updateFormLangcode($form_state); + // Invoke all specified builders for copying form values to entity fields. if (isset($form['#entity_builders'])) { foreach ($form['#entity_builders'] as $function) { diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 62b5bfb..9acf02f 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -7,8 +7,11 @@ namespace Drupal\Core\Entity; + use Drupal\Core\Language\Language; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\TypedData\LanguageFallbackData; +use Drupal\Core\TypedData\LanguageFallbackDataInterface; use Drupal\Core\TypedData\TypedDataInterface; use ArrayIterator; use InvalidArgumentException; @@ -24,7 +27,7 @@ * @todo: Once all entity types have been converted, merge improvements into the * Entity class and overhaul the EntityInterface. */ -class EntityNG extends Entity { +class EntityNG extends Entity implements LanguageFallbackDataInterface { /** * Status code indentifying a removed translation. @@ -326,6 +329,12 @@ protected function getTranslatedField($property_name, $langcode) { */ 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; + } } /** @@ -463,12 +472,23 @@ protected function getDefaultLanguage() { /** * {@inheritdoc} */ - public function onChange($property_name) { - if ($property_name == 'langcode') { + public function onChange($name) { + if ($name == 'langcode') { // Avoid using unset as this unnecessarily triggers magic methods later // on. $this->language = NULL; } + + // TODO + if (count($this->translations) > 1 && isset($this->fields[$name][$this->activeLangcode]) && empty($this->fieldDefinitions[$name]['computed'])) { + // Update values so we can detect NULL fields. + $field_value = $this->fields[$name][$this->activeLangcode]->getValue(); + // A field where we have a single NULL item is considered NULL. + if (is_array($field_value) && count($field_value) == 1 && current($field_value) === NULL) { + $field_value = NULL; + } + $this->values[$name][$this->activeLangcode] = $field_value; + } } /** @@ -569,7 +589,45 @@ protected function initializeTranslation($langcode) { /** * {@inheritdoc} */ + public function getCurrentTranslation($langcode = NULL, $context = array()) { + $translation = $this; + + // Determine the requested language. Default to the current content + // language. + if (!isset($langcode)) { + $langcode = $this->languageManager->getLanguage(Language::TYPE_CONTENT)->id; + } + + if (!empty($langcode)) { + // TODO + $context['langcode'] = $langcode; + $context['data'] = $this; + $context += array('operation' => 'entity_view'); + $candidates = $this->languageFallbackManager->getCandidates($context); + + // TODO + $default_language = $this->language ?: $this->getDefaultLanguage(); + $candidates[$default_language->id] = Language::LANGCODE_DEFAULT; + + foreach ($candidates as $candidate) { + if (isset($this->translations[$candidate])) { + $translation = $this->getTranslation($candidate); + break; + } + } + } + + return $translation; + } + + /** + * {@inheritdoc} + */ public function hasTranslation($langcode) { + $default_language = $this->language ?: $this->getDefaultLanguage(); + if ($langcode == $default_language->id) { + $langcode = Language::LANGCODE_DEFAULT; + } return !empty($this->translations[$langcode]['status']); } @@ -664,6 +722,29 @@ public function translations() { } /** + * {@inheritdoc} + * + * @return \Drupal\Core\Entity\LanguageFallbackManagerInterface + */ + public function applyLanguageFallback($context = array()) { + $return = $this; + if (count($this->translations) > 1) { + $context['langcode'] = $this->activeLangcode != Language::LANGCODE_DEFAULT ? $this->activeLangcode : $this->language->id; + $context['data'] = $this; + $context += array('operation' => 'entity_view'); + $return = new LanguageFallbackData($this->languageFallbackManager, $this, $this->values, $context); + } + return $return; + } + + /** + * {@inheritdoc} + */ + public function getPropertyLanguage($name) { + return $this->languages[$this->activeLangcode]; + } + + /** * Overrides Entity::getBCEntity(). */ public function getBCEntity() { diff --git a/core/lib/Drupal/Core/Language/FallbackManagerInterface.php b/core/lib/Drupal/Core/Language/FallbackManagerInterface.php new file mode 100644 index 0000000..209dd66 --- /dev/null +++ b/core/lib/Drupal/Core/Language/FallbackManagerInterface.php @@ -0,0 +1,35 @@ +weight) ? $a->weight : 0; $b_weight = isset($b->weight) ? $b->weight : 0; diff --git a/core/lib/Drupal/Core/Language/NoFallbackManager.php b/core/lib/Drupal/Core/Language/NoFallbackManager.php new file mode 100644 index 0000000..fe4f607 --- /dev/null +++ b/core/lib/Drupal/Core/Language/NoFallbackManager.php @@ -0,0 +1,43 @@ +languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function getCandidates(array $context = array()) { + return array(Language::LANGCODE_NOT_SPECIFIED); + } + + /** + * {@inheritdoc} + */ + public function getValuesMap(array $values, $langcode, array $context = array()) { + return array(); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/LanguageFallbackData.php b/core/lib/Drupal/Core/TypedData/LanguageFallbackData.php new file mode 100644 index 0000000..c831823 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/LanguageFallbackData.php @@ -0,0 +1,355 @@ +fallbackManager = $fallbackManager; + $this->data = $data; + $this->values = &$values; + $this->context = $context; + $this->activeLangcode = $data->language()->id; + } + else { + // TODO + throw new \InvalidArgumentException(); + } + } + + /** + * {@inheritdoc} + */ + public function getPropertyLanguage($name) { + $langcode = Language::LANGCODE_DEFAULT; + if (isset($this->context)) { + if (!isset($this->fallbackMap)) { + $this->initializeFallbackMap(); + } + if (isset($this->fallbackMap[$name])) { + $langcode = $this->fallbackMap[$name]; + } + } + if (!isset($this->languages[$langcode])) { + $this->languages += language_list(Language::STATE_ALL); + } + return $this->languages[$langcode]; + } + + /** + * TODO + */ + protected function initializeFallbackMap() { + $definitions = $this->fieldDefinitions ?: $this->data->getPropertyDefinitions(); + $keys = array_filter(array_keys($this->values), function($name) use (&$definitions) { + return !empty($definitions[$name]['translatable']); + }); + $values = array_intersect_key($this->values, array_flip($keys)); + $this->fallbackMap = $this->fallbackManager->getValuesMap($values, $this->activeLangcode, $this->context); + } + + /** + * Implements the magic method for getting object properties. + */ + public function &__get($name) { + if (!isset($this->fields[$name])) { + $translation = $this->data; + + if (!isset($this->fieldDefinitions)) { + $this->fieldDefinitions = $this->data->getPropertyDefinitions(); + } + + if (isset($this->fieldDefinitions[$name]) && !isset($this->values[$name][$this->activeLangcode])) { + if (!isset($this->fallbackMap)) { + $this->initializeFallbackMap(); + } + if (isset($this->fallbackMap[$name])) { + $langcode = $this->fallbackMap[$name]; + $translation = $this->data->getTranslation($langcode); + } + } + + $this->fields[$name] = clone $translation->__get($name); + $this->fields[$name]->setContext($name, $this); + } + + return $this->fields[$name]; + } + + /** + * Implements the magic method for setting object properties. + * + * TODO + */ + public function __set($name, $value) { + throw new \LogicException(); + } + + /** + * Implements the magic method for isset(). + */ + public function __isset($name) { + return $this->data->__isset($name); + } + + /** + * Implements the magic method for unset(). + */ + public function __unset($name) { + throw new \LogicException(); + } + + /** + * Implements the magic method for clone(). + */ + function __clone() { + $this->data = clone $this->data; + } + + /** + * {@inheritdoc} + */ + public function get($name) { + return isset($this->fields[$name]) ? $this->fields[$name] : $this->__get($name); + } + + /** + * {@inheritdoc} + */ + public function set($name, $value, $notify = TRUE) { + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function getProperties($include_computed = FALSE) { + $properties = array(); + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if ($include_computed || empty($definition['computed'])) { + $properties[$name] = $this->__get($name); + } + } + return $properties; + } + + /** + * {@inheritdoc} + */ + public function getPropertyValues() { + $values = array(); + foreach ($this->getProperties() as $name => $property) { + $values[$name] = $property->getValue(); + } + return $values; + } + + /** + * {@inheritdoc} + */ + public function setPropertyValues($values) { + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function getPropertyDefinition($name) { + return $this->data->getPropertyDefinition($name); + } + + /** + * {@inheritdoc} + */ + public function getPropertyDefinitions() { + return $this->data->getPropertyDefinitions(); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + return $this->data->isEmpty(); + } + + /** + * {@inheritdoc} + */ + public function onChange($name) { + // TODO + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + // A Traversable class can either be an IteratorAggregate or an Iterator. + return $this->data instanceof IteratorAggregate ? $this->data->getIterator() : $this->data; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->data->getType(); + } + + /** + * {@inheritdoc} + */ + public function getDefinition() { + return $this->data->getDefinition(); + } + + /** + * {@inheritdoc} + */ + public function getValue() { + return $this->data->getValue(); + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->data->setValue($value, $notify); + } + + /** + * {@inheritdoc} + */ + public function getString() { + return $this->data->getString(); + } + + /** + * {@inheritdoc} + */ + public function getConstraints() { + return $this->data->getConstraints(); + } + + /** + * {@inheritdoc} + */ + public function validate() { + return $this->data->validate(); + } + + /** + * {@inheritdoc} + */ + public function applyDefaultValue($notify = TRUE) { + return $this->data->applyDefaultValue($notify); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->data->getName(); + } + + /** + * {@inheritdoc} + */ + public function getParent() { + return $this->data->getParent(); + } + + /** + * {@inheritdoc} + */ + public function getRoot() { + return $this->data->getRoot(); + } + + /** + * {@inheritdoc} + */ + public function getPropertyPath() { + return $this->data->getPropertyPath(); + } + + /** + * {@inheritdoc} + */ + public function setContext($name = NULL, TypedDataInterface $parent = NULL) { + return $this->data->setContext($name, $parent); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/LanguageFallbackDataInterface.php b/core/lib/Drupal/Core/TypedData/LanguageFallbackDataInterface.php new file mode 100644 index 0000000..39baa50 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/LanguageFallbackDataInterface.php @@ -0,0 +1,20 @@ +entityType(); - return !empty($entity->translation[$langcode]['status']) || user_access('translate any entity', $account) || user_access("translate $entity_type entities", $account); + $info = $entity->entityInfo(); + $permission = "translate $entity_type"; + if (!empty($info['permission_granularity']) && $info['permission_granularity'] == 'bundle') { + $permission = "translate {$entity->bundle()} $entity_type"; + } + return !empty($entity->translation[$langcode]['status']) || user_access('translate any entity', $account) || user_access($permission, $account); } /** @@ -598,7 +603,7 @@ function content_translation_permission() { * Implements hook_form_alter(). */ function content_translation_form_alter(array &$form, array &$form_state) { - if (($form_controller = content_translation_form_controller($form_state)) && ($entity = $form_controller->getEntity()) && !$entity->isNew() && $entity->isTranslatable()) { + if (($form_controller = content_translation_form_controller($form_state)) && ($entity = $form_controller->getEntity()) && $entity->isTranslatable() && count($entity->getTranslationLanguages()) > 1) { $controller = content_translation_controller($entity->entityType()); $controller->entityFormAlter($form, $form_state, $entity); @@ -631,39 +636,16 @@ function content_translation_form_alter(array &$form, array &$form_state) { } /** - * Implements hook_field_language_alter(). + * Implements hook_language_fallback_candidates_OPERATION_alter(). * * Performs language fallback for unaccessible translations. */ -function content_translation_field_language_alter(&$display_language, $context) { - $entity = $context['entity']; - $entity_type = $entity->entityType(); - - if (isset($entity->translation[$context['langcode']]) && $entity->isTranslatable() && !content_translation_view_access($entity, $context['langcode'])) { - $instances = field_info_instances($entity_type, $entity->bundle()); - // Avoid altering the real entity. - $entity = clone($entity); - $entity_langcode = $entity->getUntranslated()->language()->id; - - foreach ($entity->translation as $langcode => $translation) { - if ($langcode == $context['langcode'] || !content_translation_view_access($entity, $langcode)) { - // Unset unaccessible field translations: if the field is untranslatable - // unsetting a language different from Language::LANGCODE_NOT_SPECIFIED has no - // effect. - foreach ($instances as $instance) { - // @todo BC entities have the same value accessibile both with the - // entity language and with Language::LANGCODE_DEFAULT. We need need to unset - // both until we remove the BC layer. - if ($langcode == $entity_langcode) { - unset($entity->{$instance['field_name']}[Language::LANGCODE_DEFAULT]); - } - unset($entity->{$instance['field_name']}[$langcode]); - } - } +function content_translation_language_fallback_candidates_entity_view_alter(&$candidates, $context) { + $entity = $context['data']; + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + if (!content_translation_view_access($entity, $langcode)) { + unset($candidates[$langcode]); } - - // Find the new fallback values. - field_language_fallback($display_language, $entity, $context['langcode']); } } diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationUITest.php b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationUITest.php index a83be54..9cba53e 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationUITest.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationUITest.php @@ -34,17 +34,17 @@ * Tests the basic translation UI. */ function testTranslationUI() { - $this->assertBasicTranslation(); - $this->assertOutdatedStatus(); - $this->assertPublishedStatus(); - $this->assertAuthoringInfo(); - $this->assertTranslationDeletion(); + $this->doTestBasicTranslation(); + $this->doTestOutdatedStatus(); + $this->doTestPublishedStatus(); + $this->doTestAuthoringInfo(); + $this->doTestTranslationDeletion(); } /** * Tests the basic translation workflow. */ - protected function assertBasicTranslation() { + protected function doTestBasicTranslation() { // Create a new test entity with original values in the default language. $default_langcode = $this->langcodes[0]; $values[$default_langcode] = $this->getNewEntityValues($default_langcode); @@ -101,7 +101,7 @@ protected function assertBasicTranslation() { /** * Tests up-to-date status tracking. */ - protected function assertOutdatedStatus() { + protected function doTestOutdatedStatus() { $entity = entity_load($this->entityType, $this->entityId, TRUE); $langcode = 'fr'; $default_langcode = $this->langcodes[0]; @@ -134,7 +134,7 @@ protected function assertOutdatedStatus() { /** * Tests the translation publishing status. */ - protected function assertPublishedStatus() { + protected function doTestPublishedStatus() { $entity = entity_load($this->entityType, $this->entityId, TRUE); $path = $this->controller->getEditPath($entity); @@ -156,7 +156,7 @@ protected function assertPublishedStatus() { /** * Tests the translation authoring information. */ - protected function assertAuthoringInfo() { + protected function doTestAuthoringInfo() { $entity = entity_load($this->entityType, $this->entityId, TRUE); $path = $this->controller->getEditPath($entity); $values = array(); @@ -178,8 +178,8 @@ protected function assertAuthoringInfo() { $entity = entity_load($this->entityType, $this->entityId, TRUE); foreach ($this->langcodes as $langcode) { - $this->assertEqual($entity->translation[$langcode]['uid'] == $values[$langcode]['uid'], 'Translation author correctly stored.'); - $this->assertEqual($entity->translation[$langcode]['created'] == $values[$langcode]['created'], 'Translation date correctly stored.'); + $this->assertEqual($entity->translation[$langcode]['uid'], $values[$langcode]['uid'], 'Translation author correctly stored.'); + $this->assertEqual($entity->translation[$langcode]['created'], $values[$langcode]['created'], 'Translation date correctly stored.'); } // Try to post non valid values and check that they are rejected. @@ -191,14 +191,14 @@ protected function assertAuthoringInfo() { ); $this->drupalPost($path, $edit, $this->getFormSubmitAction($entity)); $this->assertTrue($this->xpath('//div[@id="messages"]//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.'); - $this->assertEqual($entity->translation[$langcode]['uid'] == $values[$langcode]['uid'], 'Translation author correctly kept.'); - $this->assertEqual($entity->translation[$langcode]['created'] == $values[$langcode]['created'], 'Translation date correctly kept.'); + $this->assertEqual($entity->translation[$langcode]['uid'], $values[$langcode]['uid'], 'Translation author correctly kept.'); + $this->assertEqual($entity->translation[$langcode]['created'], $values[$langcode]['created'], 'Translation date correctly kept.'); } /** * Tests translation deletion. */ - protected function assertTranslationDeletion() { + protected function doTestTranslationDeletion() { // Confirm and delete a translation. $langcode = 'fr'; $entity = entity_load($this->entityType, $this->entityId, TRUE); diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 312ebb5..382f0ed 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -465,6 +465,8 @@ function hook_field_attach_view_alter(&$output, $context) { } /** + * TODO remove + * * Perform alterations on field_language() values. * * This hook is invoked to alter the array of display language codes for the @@ -477,7 +479,7 @@ function hook_field_attach_view_alter(&$output, $context) { * - entity: The entity with fields to render. * - langcode: The language code $entity has to be displayed in. */ -function hook_field_language_alter(&$display_langcode, $context) { +function _REMOVE_hook_field_language_alter(&$display_langcode, $context) { // Do not apply core language fallback rules if they are disabled or if Locale // is not registered as a translation handler. if (field_language_fallback_enabled() && field_has_translation_handler($context['entity']->entityType())) { diff --git a/core/modules/field/field.install b/core/modules/field/field.install index b425169..6e9412d 100644 --- a/core/modules/field/field.install +++ b/core/modules/field/field.install @@ -366,16 +366,12 @@ function field_update_8003() { } /** - * Moves field_storage_default and field_language_fallback to config. + * Moves field_storage_default to config. * * @ingroup config_upgrade */ function field_update_8004() { - update_variable_set('field_language_fallback', TRUE); - update_variables_to_config('field.settings', array( - 'field_storage_default' => 'default_storage', - 'field_language_fallback' => 'language_fallback', - )); + update_variables_to_config('field.settings', array('field_storage_default' => 'default_storage')); } /** diff --git a/core/modules/field/field.module b/core/modules/field/field.module index e929de6..499a9c0 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -332,6 +332,8 @@ function field_field_widget_info_alter(&$info) { } /** + * // TODO + * * Applies language fallback rules to the fields attached to the given entity. * * Core language fallback rules simply check if fields have a field translation @@ -348,30 +350,16 @@ function field_field_widget_info_alter(&$info) { * The entity to be displayed. * @param $langcode * The language code $entity has to be displayed in. + * + * @see \Drupal\Core\Language\FallbackManagerInterface::getValuesMap() + * + * @deprecated This has been deprecated in favor of the language fallback + * manager. */ function field_language_fallback(&$field_langcodes, EntityInterface $entity, $langcode) { - // Lazily init fallback candidates to avoid unnecessary calls. - $fallback_candidates = NULL; - - foreach ($field_langcodes as $field_name => $field_langcode) { - // If the requested language is defined for the current field use it, - // otherwise search for a fallback value among the fallback candidates. - if (isset($entity->{$field_name}[$langcode])) { - $field_langcodes[$field_name] = $langcode; - } - elseif (!empty($entity->{$field_name})) { - if (!isset($fallback_candidates)) { - require_once DRUPAL_ROOT . '/core/includes/language.inc'; - $fallback_candidates = language_fallback_get_candidates(); - } - foreach ($fallback_candidates as $fallback_langcode) { - if (isset($entity->{$field_name}[$fallback_langcode])) { - $field_langcodes[$field_name] = $fallback_langcode; - break; - } - } - } - } + $values = $entity->getNGEntity()->values; + $map = Drupal::service('language_fallback_manager')->getValuesMap($values, $langcode); + $field_langcodes = array_intersect_key($map, $field_langcodes); } /** diff --git a/core/modules/field/field.multilingual.inc b/core/modules/field/field.multilingual.inc index ca33a31..bc7607f 100644 --- a/core/modules/field/field.multilingual.inc +++ b/core/modules/field/field.multilingual.inc @@ -51,6 +51,8 @@ * - Provide a value in a different language as fallback. By default, the * fallback logic is applied separately to each field to ensure that there * is a value for each field to display. + * + * // TODO remove * The field language fallback logic relies on the global language fallback * configuration. Therefore, the displayed field values can be in the * requested language, but may be different if no values for the requested @@ -153,9 +155,14 @@ function field_content_languages() { /** * Checks whether field language fallback is enabled. + * + * @see \Drupal\Core\Language\FallbackManagerInterface + * + * @deprecated Language fallback is always enabled. This concept has been + * deprecated in favor of fallback contexts and manager swappability. */ function field_language_fallback_enabled() { - return language_multilingual() && config('field.settings')->get('language_fallback'); + return TRUE; } /** @@ -224,6 +231,8 @@ function field_valid_language($langcode, $default = TRUE) { } /** + * TODO remove + * * Returns the display language code for the fields attached to the given * entity. * @@ -252,59 +261,28 @@ function field_valid_language($langcode, $default = TRUE) { * @return * A language code if a field name is specified, an array of language codes * keyed by field name otherwise. + * + * @see \Drupal\Core\Language\FallbackManagerInterface::getValuesMap() + * @see \Drupal\Core\Entity\EntityInterface::getFieldLangcode() + * + * @deprecated This has been deprecated in favor of the Entity Field API. */ function field_language(EntityInterface $entity, $field_name = NULL, $langcode = NULL) { - $display_langcodes = &drupal_static(__FUNCTION__, array()); - $id = $entity->id(); - $bundle = $entity->bundle(); - $entity_type = $entity->entityType(); - $langcode = field_valid_language($langcode, FALSE); - if (!isset($display_langcodes[$entity_type][$id][$langcode])) { - $display_langcode = array(); - - // By default, display language is set to one of the locked languages - // if the field translation is not available. It is up to translation - // handlers to implement language fallback rules. - foreach (field_info_instances($entity_type, $bundle) as $instance) { - if (isset($entity->{$instance['field_name']}[$langcode])) { - $display_langcode[$instance['field_name']] = $langcode; - } - else { - // If the field has a value for one of the locked languages, then use - // that language for display. If not, the default one will be - // Language::LANGCODE_NOT_SPECIFIED. - $display_langcode[$instance['field_name']] = Language::LANGCODE_NOT_SPECIFIED; - foreach (language_list(Language::STATE_LOCKED) as $language_locked) { - if (isset($entity->{$instance['field_name']}[$language_locked->id])) { - $display_langcode[$instance['field_name']] = $language_locked->id; - break; - } - } + /* @var $data \Drupal\Core\TypedData\LanguageFallbackDataInterface */ + $translation = $entity->getCurrentTranslation($langcode); + $data = $translation->applyLanguageFallback(); + $definitions = $data->getPropertyDefinitions(); + $translatable = field_has_translation_handler($entity->entityType()); + if (!isset($field_name)) { + $display_langcodes = array(); + foreach ($definitions as $name => $definition) { + if (!empty($definition['configurable'])) { + $display_langcodes[$name] = $translatable ? $data->getPropertyLanguage($name)->id : Language::LANGCODE_NOT_SPECIFIED; } } - - if (field_has_translation_handler($entity_type)) { - $context = array( - 'entity' => $entity, - 'langcode' => $langcode, - ); - // Do not apply core language fallback rules if they are disabled or if - // the entity does not have a translation handler registered. - if (field_language_fallback_enabled() && field_has_translation_handler($entity_type)) { - field_language_fallback($display_langcode, $context['entity'], $context['langcode']); - } - drupal_alter('field_language', $display_langcode, $context); - } - - $display_langcodes[$entity_type][$id][$langcode] = $display_langcode; + return $display_langcodes; } - - $display_langcode = $display_langcodes[$entity_type][$id][$langcode]; - - // Single-field mode. - if (isset($field_name)) { - return isset($display_langcode[$field_name]) ? $display_langcode[$field_name] : FALSE; + elseif (!empty($definitions[$field_name]['configurable'])) { + return $translatable ? $data->getPropertyLanguage($field_name)->id : Language::LANGCODE_NOT_SPECIFIED; } - - return $display_langcode; } diff --git a/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php b/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php index 7ce432b..226dc58 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php +++ b/core/modules/field/lib/Drupal/field/Plugin/views/field/Field.php @@ -8,6 +8,7 @@ namespace Drupal\field\Plugin\views\field; use Drupal\Core\Language\Language; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\field\Plugin\Type\Formatter\FormatterPluginManager; use Drupal\views\ViewExecutable; @@ -76,6 +77,13 @@ class Field extends FieldPluginBase { protected $formatterPluginManager; /** + * The language fallback manager. + * + * @var \Drupal\Core\Language\FallbackManagerInterface; + */ + protected $languageFallbackManager; + + /** * Constructs a \Drupal\field\Plugin\views\field\Field object. * * @param array $configuration @@ -87,10 +95,11 @@ class Field extends FieldPluginBase { * @param \Drupal\field\Plugin\Type\Formatter\FormatterPluginManager $formatter_plugin_manager * The field formatter plugin manager. */ - public function __construct(array $configuration, $plugin_id, array $plugin_definition, FormatterPluginManager $formatter_plugin_manager) { + public function __construct(array $configuration, $plugin_id, array $plugin_definition, FormatterPluginManager $formatter_plugin_manager, FallbackManagerInterface $fallback_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->formatterPluginManager = $formatter_plugin_manager; + $this->languageFallbackManager = $fallback_manager; } /** @@ -101,7 +110,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('plugin.manager.field.formatter') + $container->get('plugin.manager.field.formatter'), + $container->get('language_fallback_manager') ); } @@ -231,14 +241,7 @@ public function query($use_groupby = FALSE) { array(drupal_container()->get(Language::TYPE_CONTENT)->id, $default_langcode), $this->view->display_handler->options['field_langcode']); $placeholder = $this->placeholder(); - $langcode_fallback_candidates = array($langcode); - if (field_language_fallback_enabled()) { - require_once DRUPAL_ROOT . '/includes/language.inc'; - $langcode_fallback_candidates = array_merge($langcode_fallback_candidates, language_fallback_get_candidates()); - } - else { - $langcode_fallback_candidates[] = Language::LANGCODE_NOT_SPECIFIED; - } + $langcode_fallback_candidates = $this->languageFallbackManager->getCandidates(array('operation' => 'views_query', 'langcode' => $langcode, 'data' => $this)); $this->query->addWhereExpression(0, "$column IN($placeholder) OR $column IS NULL", array($placeholder => $langcode_fallback_candidates)); } } @@ -836,11 +839,13 @@ function field_langcode(EntityInterface $entity) { array(drupal_container()->get(Language::TYPE_CONTENT)->id, $default_langcode), $this->view->display_handler->options['field_language']); - // Give the Field Language API a chance to fallback to a different language - // (or Language::LANGCODE_NOT_SPECIFIED), in case the field has no data for the selected language. - // field_view_field() does this as well, but since the returned language code - // is used before calling it, the fallback needs to happen explicitly. - $langcode = field_language($entity, $this->field_info['field_name'], $langcode); + // Give the Entity Field API a chance to fallback to a different language + // (or Language::LANGCODE_NOT_SPECIFIED), in case the field has no data + // for the selected language. field_view_field() does this as well, but + // since the returned language code is used before calling it, the + // fallback needs to happen explicitly. + $name = $this->field_info['field_name']; + $langcode = $entity->getCurrentTranslation($langcode)->applyLanguageFallback()->getPropertyLanguage($name)->id; return $langcode; } diff --git a/core/modules/field/lib/Drupal/field/Tests/TranslationTest.php b/core/modules/field/lib/Drupal/field/Tests/TranslationTest.php index ebf9722..f09a172 100644 --- a/core/modules/field/lib/Drupal/field/Tests/TranslationTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/TranslationTest.php @@ -80,6 +80,8 @@ public static function getInfo() { function setUp() { parent::setUp(); + $this->installConfig(array('language')); + $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); $this->entity_type = 'entity_test'; @@ -158,6 +160,7 @@ function testTranslatableFieldSaveLoad() { $field_translations = array(); $available_langcodes = field_available_languages($entity_type, $this->field); $this->assertTrue(count($available_langcodes) > 1, 'Field is translatable.'); + $available_langcodes = array_keys(language_list()); $entity->langcode->value = reset($available_langcodes); foreach ($available_langcodes as $langcode) { $field_translations[$langcode] = $this->_generateTestFieldValues($this->field['cardinality']); @@ -258,18 +261,14 @@ function testFieldDisplayLanguage() { ); entity_create('field_instance', $instance)->save(); - $enabled_langcodes = field_content_languages(); + $enabled_langcodes = array_keys(language_list()); $entity = entity_create($entity_type, array('id' => 1, 'revision_id' => 1, 'type' => $this->instance['bundle']));; $entity->langcode->value = reset($enabled_langcodes); $instances = field_info_instances($entity_type, $this->instance['bundle']); - $langcodes = array(); - // This array is used to store, for each field name, which one of the locked - // languages will be used for display. - $locked_languages = array(); - // Generate field translations for languages different from the first // enabled. + $langcodes = array(); foreach ($instances as $instance) { $field_name = $instance['field_name']; $field = field_info_field($field_name); @@ -281,30 +280,19 @@ function testFieldDisplayLanguage() { while (isset($langcodes[$langcode])); $langcodes[$langcode] = TRUE; $entity->getTranslation($langcode)->{$field_name}->setValue($this->_generateTestFieldValues($field['cardinality'])); - // If the langcode is one of the locked languages, then that one - // will also be used for display. Otherwise, the default one should be - // used, which is Language::LANGCODE_NOT_SPECIFIED. - if (language_is_locked($langcode)) { - $locked_languages[$field_name] = $langcode; - } - else { - $locked_languages[$field_name] = Language::LANGCODE_NOT_SPECIFIED; - } } // Test multiple-fields display languages for untranslatable entities. field_test_entity_info_translatable($entity_type, FALSE); - drupal_static_reset('field_language'); $requested_langcode = $enabled_langcodes[0]; $display_langcodes = field_language($entity, NULL, $requested_langcode); foreach ($instances as $instance) { $field_name = $instance['field_name']; - $this->assertTrue($display_langcodes[$field_name] == $locked_languages[$field_name], format_string('The display language for field %field_name is %language.', array('%field_name' => $field_name, '%language' => $locked_languages[$field_name]))); + $this->assertTrue($display_langcodes[$field_name] == Language::LANGCODE_NOT_SPECIFIED, format_string('The display language for field %field_name is %language.', array('%field_name' => $field_name, '%language' => Language::LANGCODE_NOT_SPECIFIED))); } // Test multiple-fields display languages for translatable entities. field_test_entity_info_translatable($entity_type, TRUE); - drupal_static_reset('field_language'); $display_langcodes = field_language($entity, NULL, $requested_langcode); foreach ($instances as $instance) { $field_name = $instance['field_name']; @@ -312,19 +300,19 @@ function testFieldDisplayLanguage() { // As the requested language was not assinged to any field, if the // returned language is defined for the current field, core fallback rules // were successfully applied. - $this->assertTrue(!empty($entity->getTranslation($langcode)->{$field_name}) && $langcode != $requested_langcode, format_string('The display language for the field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode))); + $value = $entity->getTranslation($langcode)->{$field_name}->getValue(); + $this->assertTrue(!empty($value) && $langcode != $requested_langcode, format_string('The display language for the field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode))); } // Test single-field display language. - drupal_static_reset('field_language'); $langcode = field_language($entity, $this->field_name, $requested_langcode); - $this->assertTrue(!empty($entity->getTranslation($langcode)->{$this->field_name}) && $langcode != $requested_langcode, format_string('The display language for the (single) field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode))); + $value = $entity->getTranslation($langcode)->{$this->field_name}->getValue(); + $this->assertTrue(!empty($value) && $langcode != $requested_langcode, format_string('The display language for the (single) field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode))); // Test field_language() basic behavior without language fallback. - \Drupal::state()->set('field_test.language_fallback', FALSE); + $this->container->get('config.factory')->get('language.fallback')->set('enabled', FALSE); $entity->getTranslation($requested_langcode)->{$this->field_name}->value = mt_rand(1, 127); - drupal_static_reset('field_language'); - $display_langcode = field_language($entity->getBCEntity(), $this->field_name, $requested_langcode); + $display_langcode = field_language($entity, $this->field_name, $requested_langcode); $this->assertEqual($display_langcode, $requested_langcode, 'Display language behave correctly when language fallback is disabled'); } diff --git a/core/modules/field/tests/modules/field_test/field_test.module b/core/modules/field/tests/modules/field_test/field_test.module index 800f4d4..949bd93 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -67,15 +67,6 @@ function field_test_field_available_languages_alter(&$langcodes, $context) { } /** - * Implements hook_field_language_alter(). - */ -function field_test_field_language_alter(&$display_langcode, $context) { - if (Drupal::state()->get('field_test.language_fallback') ?: TRUE) { - field_language_fallback($display_langcode, $context['entity'], $context['langcode']); - } -} - -/** * Store and retrieve keyed data for later verification by unit tests. * * This function is a simple in-memory key-value store with the diff --git a/core/modules/language/config/language.fallback.yml b/core/modules/language/config/language.fallback.yml new file mode 100644 index 0000000..d4ca941 --- /dev/null +++ b/core/modules/language/config/language.fallback.yml @@ -0,0 +1 @@ +enabled: true diff --git a/core/modules/language/config/schema/language.schema.yml b/core/modules/language/config/schema/language.schema.yml index d50d121..80d57d0 100644 --- a/core/modules/language/config/schema/language.schema.yml +++ b/core/modules/language/config/schema/language.schema.yml @@ -74,3 +74,8 @@ language.entity.*: langcode: type: string label: 'Language code' + +language.fallback: + enabled: + type: boolean + label: 'Enabled' diff --git a/core/modules/language/lib/Drupal/language/FallbackManager.php b/core/modules/language/lib/Drupal/language/FallbackManager.php new file mode 100644 index 0000000..d6e01ec --- /dev/null +++ b/core/modules/language/lib/Drupal/language/FallbackManager.php @@ -0,0 +1,120 @@ +config = $config; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function getCandidates(array $context = array()) { + // Get languages ordered by weight, add Language::LANGCODE_NOT_SPECIFIED at + // the end. + $candidates = array_keys(language_list()); + $candidates[] = Language::LANGCODE_NOT_SPECIFIED; + $candidates = MapArray::copyValuesToKeys($candidates); + + // The first candidate should always be the requested language if any. + if (!empty($context['langcode'])) { + $langcode = $context['langcode']; + $candidates = array($langcode => $langcode) + $candidates; + } + + // Let other modules hook in and add/change candidates. + $this->alter('language_fallback_candidates', $candidates, $context); + + return $candidates; + } + + /** + * {@inheritdoc} + */ + public function getValuesMap(array $values, $langcode, array $context = array()) { + $map = array(); + $enabled = $this->config->get('language.fallback')->get('enabled'); + + // Lazily init fallback candidates to avoid unnecessary calls. + $candidates = NULL; + foreach ($values as $name => $value) { + // TODO + $map[$name] = $langcode; + + // If the requested language is defined for the current value use it, + // otherwise search for a fallback value among the fallback candidates, if + // language fallback is enabled. + if ($enabled && is_array($value) && !empty($values[$name]) && empty($values[$name][$langcode])) { + if (!isset($candidates)) { + $context['langcode'] = $langcode; + $candidates = $this->getCandidates($context); + // Remove the requested language as we are already testing it above. + unset($candidates[$langcode]); + } + foreach ($candidates as $fallback_langcode) { + if (!empty($values[$name][$fallback_langcode])) { + $map[$name] = $fallback_langcode; + break; + } + } + } + } + + // TODO + if ($enabled) { + $context = array('values' => $values, 'langcode' => $langcode) + $context; + $this->alter('language_fallback_values', $map, $context); + } + + return $map; + } + + /** + * TODO + */ + protected function alter($type, &$data, array $context) { + $types = array($type); + if (!empty($context['operation'])) { + $types[] = $type . '_' . $context['operation']; + } + return $this->moduleHandler->alter($types, $data, $context); + } + +} diff --git a/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php b/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php new file mode 100644 index 0000000..45b4ee0 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php @@ -0,0 +1,37 @@ +getDefinition('language_fallback_manager'); + $definition->setClass('Drupal\language\FallbackManager'); + $definition->addArgument(new Reference('config.factory')); + $definition->addArgument(new Reference('module_handler')); + } + +} diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index b781421..d427d78 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -971,6 +971,19 @@ function locale_update_8017() { } /** + * Moves the field language fallback settings to config. + * + * @ingroup config_upgrade + */ +function locale_update_8018() { + $value = update_variable_get('locale_field_language_fallback'); + if (!isset($value)) { + update_variable_set('locale_field_language_fallback', TRUE); + } + update_variables_to_config('language.fallback', array('locale_field_language_fallback' => 'enabled')); +} + +/** * @} End of "addtogroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php index 43d54ce..80ee321 100644 --- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php @@ -11,6 +11,8 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Database\Connection; +use Drupal\Core\Language\FallbackManagerInterface; +use Drupal\Core\Language\LanguageManager; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Cmf\Component\Routing\RouteProviderInterface; @@ -55,8 +57,8 @@ class MenuLinkStorageController extends DatabaseStorageController implements Men * @param \Symfony\Cmf\Component\Routing\RouteProviderInterface $route_provider * The route provider service. */ - public function __construct($entity_type, array $entity_info, Connection $database, RouteProviderInterface $route_provider) { - parent::__construct($entity_type, $entity_info, $database); + public function __construct($entity_type, array $entity_info, Connection $database, LanguageManager $language_manager, FallbackManagerInterface $fallback_manager, RouteProviderInterface $route_provider) { + parent::__construct($entity_type, $entity_info, $database, $language_manager, $fallback_manager); $this->routeProvider = $route_provider; @@ -85,6 +87,8 @@ public static function createInstance(ContainerInterface $container, $entity_typ $entity_type, $entity_info, $container->get('database'), + $container->get('language_manager'), + $container->get('language_fallback_manager'), $container->get('router.route_provider') ); } diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php index dac3575..e0ed87a 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php @@ -78,7 +78,7 @@ protected function getFormSubmitAction(EntityInterface $entity) { /** * Overrides \Drupal\content_translation\Tests\ContentTranslationUITest::assertPublishedStatus(). */ - protected function assertPublishedStatus() { + protected function doTestPublishedStatus() { $entity = entity_load($this->entityType, $this->entityId, TRUE); $path = $this->controller->getEditPath($entity); $languages = language_list(); @@ -110,7 +110,7 @@ protected function assertPublishedStatus() { /** * Overrides \Drupal\content_translation\Tests\ContentTranslationUITest::assertAuthoringInfo(). */ - protected function assertAuthoringInfo() { + protected function doTestAuthoringInfo() { $entity = entity_load($this->entityType, $this->entityId, TRUE); $path = $this->controller->getEditPath($entity); $languages = language_list(); diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 5a6d53a..db27aa0 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -132,9 +132,9 @@ function node_help($path, $arg) { return '

' . t('Revisions allow you to track differences between multiple versions of your content, and revert back to older versions.') . '

'; case 'node/%/edit': - $node = node_load($arg[1]); - $type = node_type_load($node->bundle()); - return (!empty($type->help) ? '

' . filter_xss_admin($type->help) . '

' : ''); +// $node = node_load($arg[1]); +// $type = node_type_load($node->bundle()); +// return (!empty($type->help) ? '

' . filter_xss_admin($type->help) . '

' : ''); } if ($arg[0] == 'node' && $arg[1] == 'add' && $arg[2]) { diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc index 77dc994..198dff8 100644 --- a/core/modules/node/node.tokens.inc +++ b/core/modules/node/node.tokens.inc @@ -139,7 +139,7 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr case 'summary': if ($items = field_get_items($node, 'body', $langcode)) { $instance = field_info_instance('node', 'body', $node->type); - $field_langcode = field_language($node, 'body', $langcode); + $field_langcode = $node->getCurrentTranslation($langcode)->language()->id; // If the summary was requested and is not empty, use it. if ($name == 'summary' && !empty($items[0]['summary'])) { 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 40a7e44..6610571 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -82,6 +82,7 @@ function setUp() { $language = new Language(array( 'id' => 'l' . $i, 'name' => $this->randomString(), + 'weight' => $i, )); $this->langcodes[$i] = $language->id; language_save($language); @@ -462,4 +463,71 @@ function testEntityTranslationAPI() { $this->assertEqual($translation->get($this->field_name)->value, $this->field_name . '_' . $langcode2, 'Language-aware default values correctly populated.'); } + /** + * Tests language fallback applied to field and entity translations. + */ + function testLanguageFallback() { + $default_langcode = $this->langcodes[0]; + $langcode = $this->langcodes[1]; + $langcode2 = $this->langcodes[2]; + + $values = array(); + for ($i = 0; $i < 3; $i++) { + $values[$this->langcodes[$i]]['name'] = $this->randomName(); + $values[$this->langcodes[$i]]['user_id'] = mt_rand(0, 127); + } + + $entity = $this->entityManager + ->getStorageController('entity_test_mul') + ->create(array('langcode' => $default_langcode) + $values[$default_langcode]); + $entity->save(); + + $entity->addTranslation($langcode, $values[$langcode]); + $entity->save(); + + // Check that retrieveing the current translation works as expected. + $entity = $this->reloadEntity($entity); + $translation = $entity->getCurrentTranslation($langcode2); + $this->assertEqual($translation->language()->id, $default_langcode, 'The current translation language matches the expected one.'); + + // TODO + $languages = language_list(); + $languages[$langcode]->weight = -1; + language_save($languages[$langcode]); + $translation = $entity->getCurrentTranslation($langcode2); + $this->assertEqual($translation->language()->id, $langcode, 'The current translation language matches the expected one.'); + + // TODO + $translation = $entity->getTranslation($langcode); + unset($translation->name); + $translation->save(); + $entity = $this->reloadEntity($entity); + $translation = $entity->getCurrentTranslation($langcode); + $this->assertEqual($translation->applyLanguageFallback()->name->value, $values[$default_langcode]['name'], 'Field fallback is correctly performed when a field translation is missing.'); + + // TODO + $translation2 = $entity->addTranslation($langcode2, $values[$langcode2]); + unset($translation2->name); + $translation2->save(); + $entity = $this->reloadEntity($entity); + $translation2 = $entity->getCurrentTranslation($langcode2); + $this->assertEqual($translation2->applyLanguageFallback()->name->value, $values[$default_langcode]['name'], 'Field fallback is correctly performed when a field translation is missing.'); + + // TODO + $translation = $entity->getTranslation($langcode); + $translation->name->value = $values[$langcode]['name']; + $data = $translation2->applyLanguageFallback(); + $this->assertEqual($data->name->value, $values[$langcode]['name'], 'Field fallback is correctly performed when a field translation is missing.'); + + // TODO + try { + $message = 'Trying to set a value on the fallback data causes an exception to be thrown.'; + $data->name->value = $this->randomName(); + $this->fail($message); + } + catch (\LogicException $e) { + $this->pass($message); + } + } + } diff --git a/core/modules/user/lib/Drupal/user/UserStorageController.php b/core/modules/user/lib/Drupal/user/UserStorageController.php index acf9d5b..3bc13a5 100644 --- a/core/modules/user/lib/Drupal/user/UserStorageController.php +++ b/core/modules/user/lib/Drupal/user/UserStorageController.php @@ -7,6 +7,8 @@ namespace Drupal\user; +use Drupal\Core\Language\FallbackManagerInterface; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\Entity\EntityBCDecorator; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Password\PasswordInterface; @@ -51,8 +53,8 @@ class UserStorageController extends DatabaseStorageControllerNG implements UserS * @param \Drupal\user\UserDataInterface $user_data * The user data service. */ - public function __construct($entity_type, $entity_info, Connection $database, PasswordInterface $password, UserDataInterface $user_data) { - parent::__construct($entity_type, $entity_info, $database); + public function __construct($entity_type, $entity_info, Connection $database, LanguageManager $languageManager, FallbackManagerInterface $fallback_manager, PasswordInterface $password, UserDataInterface $user_data) { + parent::__construct($entity_type, $entity_info, $database, $languageManager, $fallback_manager); $this->password = $password; $this->userData = $user_data; @@ -66,6 +68,8 @@ public static function createInstance(ContainerInterface $container, $entity_typ $entity_type, $entity_info, $container->get('database'), + $container->get('language_manager'), + $container->get('language_fallback_manager'), $container->get('password'), $container->get('user.data') ); diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php index 78bbeca..9b174b5 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewUI.php @@ -11,6 +11,8 @@ use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\views\ViewExecutable; use Drupal\Core\Database\Database; +use Drupal\Core\Language\FallbackManagerInterface; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\Session\AccountInterface; use Drupal\views\Plugin\views\query\Sql; @@ -905,6 +907,20 @@ public function setNewRevision($value = TRUE) { } /** + * {@inheritdoc} + */ + public function setLanguageManager(LanguageManager $language_manager) { + $this->storage->setLanguageManager($language_manager); + } + + /** + * {@inheritdoc} + */ + public function setLanguageFallbackManager(FallbackManagerInterface $fallback_manager) { + $this->storage->setLanguageFallbackManager($fallback_manager); + } + + /** * Implements \Drupal\Core\Entity\EntityInterface::enforceIsNew(). */ public function enforceIsNew($value = TRUE) { @@ -927,6 +943,14 @@ public function getTranslation($langcode) { } /** + * {@inheritdoc} + */ + public function getCurrentTranslation($langcode = NULL, $context = array()) { + // @todo Revisit this once config entities are converted to NG. + return $this; + } + + /** * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages(). */ public function getTranslationLanguages($include_default = TRUE) { @@ -1243,4 +1267,12 @@ public function mergeDefaultDisplaysOptions() { public function uriRelationships() { return $this->storage->uriRelationships(); } + + /** + * {@inheritdoc} + */ + public function applyLanguageFallback($context = array()) { + return $this->storage->applyLanguageFallback($context); + } + }