diff --git a/core/core.services.yml b/core/core.services.yml index 79f1fc1..4009467 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -200,6 +200,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 79bae62..76a9512 100644 --- a/core/includes/language.inc +++ b/core/includes/language.inc @@ -544,20 +544,14 @@ function language_url_split_prefix($path, $languages) { * * @return * An array of language codes. + * + * @see \Drupal\Core\Language\FallbackManagerInterface::getCandidates() + * + * @deprecated This has been deprecated 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('type' => $type)); } /** diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index 4bd89ae..3658161 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -10,9 +10,11 @@ use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\Query\QueryInterface; -use Drupal\Core\Language\Language; use Drupal\Component\Utility\NestedArray; use Drupal\Component\Uuid\Uuid; +use Drupal\Core\Language\FallbackManagerInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use Drupal\field\FieldInfo; use Drupal\field\FieldUpdateForbiddenException; use Drupal\field\FieldInterface; @@ -77,6 +79,20 @@ class DatabaseStorageController extends FieldableEntityStorageControllerBase { protected $fieldInfo; /** + * 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) { @@ -85,7 +101,9 @@ public static function createInstance(ContainerInterface $container, $entity_typ $entity_info, $container->get('database'), $container->get('field.info'), - $container->get('uuid') + $container->get('uuid'), + $container->get('language_manager'), + $container->get('language_fallback_manager') ); } @@ -102,13 +120,19 @@ public static function createInstance(ContainerInterface $container, $entity_typ * The field info service. * @param \Drupal\Component\Uuid\UuidInterface $uuid_service * The UUID service. + * @param \Drupal\Core\Language\LanguageManager + * The language manager. + * @param \Drupal\Core\Language\FallbackManagerInterface + * The language fallback manager. */ - public function __construct($entity_type, array $entity_info, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service) { + public function __construct($entity_type, array $entity_info, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service, LanguageManager $language_manager, FallbackManagerInterface $fallback_manager) { parent::__construct($entity_type, $entity_info); $this->database = $database; $this->fieldInfo = $field_info; $this->uuidService = $uuid_service; + $this->languageManager = $language_manager; + $this->languageFallbackManager = $fallback_manager; // Check if the entity type supports IDs. if (isset($this->entityInfo['entity_keys']['id'])) { @@ -389,6 +413,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})) { @@ -891,8 +917,8 @@ public function onBundleRename($bundle, $bundle_new) { ->fields(array('bundle' => $bundle_new)) ->condition('bundle', $bundle) ->execute(); + } } - } /** * {@inheritdoc} diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index 02b9978..f41baab 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 Drupal\field\FieldInfo; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Entity\EntityInterface; @@ -52,8 +54,9 @@ class DatabaseStorageControllerNG extends DatabaseStorageController { /** * Overrides DatabaseStorageController::__construct(). */ - public function __construct($entity_type, array $entity_info, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service) { - parent::__construct($entity_type,$entity_info, $database, $field_info, $uuid_service); + public function __construct($entity_type, array $entity_info, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service, LanguageManager $languageManager, FallbackManagerInterface $fallback_manager) { + parent::__construct($entity_type, $entity_info, $database, $field_info, $uuid_service, $languageManager, $fallback_manager); + $this->bundleKey = !empty($this->entityInfo['entity_keys']['bundle']) ? $this->entityInfo['entity_keys']['bundle'] : FALSE; $this->entityClass = $this->entityInfo['class']; @@ -108,6 +111,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])) { @@ -247,6 +252,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); @@ -318,6 +325,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 6d41d13..1bbc6a4 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\Core\Entity\Plugin\DataType\EntityReferenceItem; +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 Drupal\Core\Session\AccountInterface; @@ -59,6 +61,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 find an existing translation or + * existing field values when the desired ones are not available. + * + * @var \Drupal\Core\Language\FallbackManagerInterface + */ + protected $languageFallbackManager; + + /** * Constructs an Entity object. * * @param array $values @@ -119,6 +137,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() { @@ -375,8 +407,22 @@ public function language() { * @return \Drupal\Core\Entity\EntityInterface */ public function getTranslation($langcode) { - // @todo: Replace by EntityNG implementation once all entity types have been - // converted to use the entity field API. + return $this; + } + + /** + * Implements \Drupal\Core\TypedData\TranslatableInterface::getExistingTranslation(). + * + * @return \Drupal\Core\Entity\EntityInterface + */ + public function getExistingTranslation($langcode, $context = array()) { + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCurrentTranslation() { return $this; } diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index 552813b..04fcfc5 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -348,27 +348,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']; - } - 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); - } + public function getFormLangcode(array &$form_state) { + if (empty($form_state['langcode'])) { + $form_state['langcode'] = $this->entity->getCurrentTranslation()->language()->id; } - - // 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']; } /** @@ -402,6 +386,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; } @@ -419,7 +405,7 @@ public function getEntity() { * @param array $form_state * A keyed array containing the current state of the form. */ - protected function getTranslatedEntity(array $form_state) { + protected function getTranslatedEntity(array &$form_state) { $langcode = $this->getFormLangcode($form_state); return $this->entity->getTranslation($langcode); } diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php index 07d8af1..6ad106b 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php @@ -26,7 +26,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 11396d9..1863c99 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php @@ -58,6 +58,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 49fa3a0..1af9616 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -258,6 +258,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; + } } /** @@ -501,6 +507,44 @@ protected function initializeTranslation($langcode) { /** * {@inheritdoc} */ + public function getExistingTranslation($langcode, $context = array()) { + $translation = $this; + + if (!empty($langcode)) { + // Retrieve language fallback candidates to perform the entity language + // negotiation. + $context['langcode'] = $langcode; + $context['data'] = $this; + $context += array('operation' => 'entity_view'); + $candidates = $this->languageFallbackManager->getCandidates($context); + + // Ensure the default language has the proper language code. + $default_language = $this->language ?: $this->getDefaultLanguage(); + $candidates[$default_language->id] = Language::LANGCODE_DEFAULT; + + // Return the most fitting entity translation. + foreach ($candidates as $candidate) { + if ($this->hasTranslation($candidate)) { + $translation = $this->getTranslation($candidate); + break; + } + } + } + + return $translation; + } + + /** + * {@inheritdoc} + */ + public function getCurrentTranslation() { + $langcode = $this->languageManager->getLanguage(Language::TYPE_CONTENT)->id; + return $this->getExistingTranslation($langcode); + } + + /** + * {@inheritdoc} + */ public function hasTranslation($langcode) { $default_language = $this->language ?: $this->getDefaultLanguage(); if ($langcode == $default_language->id) { diff --git a/core/lib/Drupal/Core/Language/FallbackManagerInterface.php b/core/lib/Drupal/Core/Language/FallbackManagerInterface.php new file mode 100644 index 0000000..bad51b6 --- /dev/null +++ b/core/lib/Drupal/Core/Language/FallbackManagerInterface.php @@ -0,0 +1,36 @@ +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..0f0d4d9 --- /dev/null +++ b/core/lib/Drupal/Core/Language/NoFallbackManager.php @@ -0,0 +1,39 @@ +languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function getCandidates(array $context = array()) { + return array(Language::LANGCODE_NOT_SPECIFIED); + } + +} diff --git a/core/lib/Drupal/Core/TypedData/TranslatableInterface.php b/core/lib/Drupal/Core/TypedData/TranslatableInterface.php index 0d5332b..5a83658 100644 --- a/core/lib/Drupal/Core/TypedData/TranslatableInterface.php +++ b/core/lib/Drupal/Core/TypedData/TranslatableInterface.php @@ -7,6 +7,9 @@ namespace Drupal\Core\TypedData; +use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Language\FallbackManagerInterface; + /** * Interface for translatable data. */ @@ -21,6 +24,22 @@ public function language(); /** + * Sets the language manager to be used when dealing with languages. + * + * @param \Drupal\Core\Language\LanguageManager + * The language manager. + */ + public function setLanguageManager(LanguageManager $language_manager); + + /** + * Sets the language fallback manager. + * + * @param \Drupal\Core\Language\FallbackManagerInterface + * The language fallback manager. + */ + public function setLanguageFallbackManager(FallbackManagerInterface $fallback_manager); + + /** * Returns the languages the data is translated to. * * @param bool $include_default @@ -35,10 +54,9 @@ public function getTranslationLanguages($include_default = TRUE); /** * Gets a translation of the data. * - * The returned translation has to be implement the same typed data interfaces - * as this typed data object, excluding the TranslatableInterface. E.g., if - * this typed data object implements the ComplexDataInterface and - * AccessibleInterface, the translation object has to implement both as well. + * The returned translation has to be of the same type than this typed data + * object. If the specified translation does not exist, a new one will be + * instantiated. * * @param $langcode * The language code of the translation to get or Language::LANGCODE_DEFAULT @@ -49,6 +67,36 @@ public function getTranslationLanguages($include_default = TRUE); */ public function getTranslation($langcode); + /** + * Returns the translation to be used in the given context. + * + * This will check whether a translation for the desired language is available + * and if not, it will fall back to the most appropriate translation based on + * the provided context. + * + * @param string $language + * The language code of the translation to be retrieved. + * @param array $context + * (optional) An associative array of arbitrary data that can be useful to + * determine the proper fallback sequence. + * + * @return \Drupal\Core\TypedData\TypedDataInterface + * A typed data object for the translated data. + * + * @see \Drupal\Core\Language\FallbackManagerInterface + */ + public function getExistingTranslation($langcode, $context = array()); + + /** + * Returns the current translation. + * + * If there is no translation for the current language a fallback translation + * is returned. + * + * @return \Drupal\Core\TypedData\TypedDataInterface + * A typed data object for the translated data. + */ + public function getCurrentTranslation(); /** * Returns the translatable object referring to the original language. diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php index 8256889..9b68a82 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentFormController.php +++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php @@ -17,6 +17,22 @@ class CommentFormController extends EntityFormControllerNG { /** + * {@inheritdoc} + */ + protected function init(array &$form_state) { + $comment = $this->entity; + + // Make the comment inherit the current content language unless specifically + // set. + if ($comment->isNew()) { + $language_content = language(Language::TYPE_CONTENT); + $comment->langcode->value = $language_content->id; + } + + parent::init($form_state); + } + + /** * Overrides Drupal\Core\Entity\EntityFormController::form(). */ public function form(array $form, array &$form_state) { @@ -159,13 +175,6 @@ public function form(array $form, array &$form_state) { '#value' => ($comment->id() ? !$comment->uid->target_id : $user->isAnonymous()), ); - // Make the comment inherit the current content language unless specifically - // set. - if ($comment->isNew()) { - $language_content = language(Language::TYPE_CONTENT); - $comment->langcode->value = $language_content->id; - } - // Add internal comment properties. $original = $comment->getUntranslated(); foreach (array('cid', 'pid', 'nid', 'uid', 'node_type', 'langcode') as $key) { diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index a6cf31c..b131e24 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -282,7 +282,12 @@ function content_translation_translate_access(EntityInterface $entity) { */ function content_translation_view_access(EntityInterface $entity, $langcode, AccountInterface $account = NULL) { $entity_type = $entity->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); } /** @@ -586,7 +591,10 @@ 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()) { + $form_controller = content_translation_form_controller($form_state); + $entity = $form_controller ? $form_controller->getEntity() : NULL; + + if ($entity && $entity->isTranslatable() && count($entity->getTranslationLanguages()) > 1) { $controller = content_translation_controller($entity->entityType()); $controller->entityFormAlter($form, $form_state, $entity); @@ -609,28 +617,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)) { - $entity->removeTranslation($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 ef579b2..4fd6a5b 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->drupalPostForm($path, $edit, $this->getFormSubmitAction($entity)); $this->assertTrue($this->xpath('//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/config/field.settings.yml b/core/modules/field/config/field.settings.yml index 6b569a9..b6172c1 100644 --- a/core/modules/field/config/field.settings.yml +++ b/core/modules/field/config/field.settings.yml @@ -1,2 +1 @@ -language_fallback: '1' purge_batch_size: 10 diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml index 3549177..9b8ba92 100644 --- a/core/modules/field/config/schema/field.schema.yml +++ b/core/modules/field/config/schema/field.schema.yml @@ -4,9 +4,6 @@ field.settings: type: mapping label: 'Field settings' mapping: - language_fallback: - type: boolean - label: 'Whether the field display falls back to global language fallback configuration' purge_batch_size: type: integer label: 'Maximum number of field data records to purge' diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 50b5edb..652b012 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -415,27 +415,6 @@ function hook_field_attach_view_alter(&$output, $context) { } /** - * Perform alterations on field_language() values. - * - * This hook is invoked to alter the array of display language codes for the - * given entity. - * - * @param $display_langcode - * A reference to an array of language codes keyed by field name. - * @param $context - * An associative array containing: - * - 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) { - // 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())) { - field_language_fallback($display_langcode, $context['entity'], $context['langcode']); - } -} - -/** * Alter field_available_languages() values. * * This hook is invoked from field_available_languages() to allow modules to diff --git a/core/modules/field/field.deprecated.inc b/core/modules/field/field.deprecated.inc index e33b150..6bb85e9 100644 --- a/core/modules/field/field.deprecated.inc +++ b/core/modules/field/field.deprecated.inc @@ -829,3 +829,92 @@ function field_get_items(EntityInterface $entity, $field_name, $langcode = NULL) function field_get_default_value(EntityInterface $entity, $field, $instance, $langcode = NULL) { return $instance->getFieldDefaultValue($entity); } + +/** + * Checks whether field language fallback is enabled. + * + * @see \Drupal\Core\Language\FallbackManagerInterface + * + * @deprecated Use the language fallback manager service instead of directly + * checking whether language fallback is enabled. + */ +function field_language_fallback_enabled() { + return language_multilingual() && Drupal::service('language_fallback_manager')->isEnabled(); +} + +/** + * Ensures that a given language code is valid. + * + * Checks whether the given language code is one of the enabled language codes. + * Otherwise, it returns the current, global language code; or the site's + * default language code, if the additional parameter $default is TRUE. + * + * @param $langcode + * The language code to validate. + * @param $default + * Whether to return the default language code or the current language code in + * case $langcode is invalid. + * + * @return + * A valid language code. + * + * @deprecated This has been deprecated in favor of the Entity Field API. + */ +function field_valid_language($langcode, $default = TRUE) { + $languages = field_content_languages(); + if (in_array($langcode, $languages)) { + return $langcode; + } + return $default ? language_default()->id : language(Language::TYPE_CONTENT)->id; +} + +/** + * Returns the display language code for the fields attached to the given + * entity. + * + * The actual language code for each given field is determined based on the + * requested language code and the actual data available in the fields + * themselves. + * If there is no registered translation handler for the given entity type, the + * display language code to be used is just Language::LANGCODE_NOT_SPECIFIED, as no other + * language code is allowed by field_available_languages(). + * + * If translation handlers are found, we let modules provide alternative display + * language codes for fields not having the requested language code available. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be displayed. + * @param $field_name + * (optional) The name of the field to be displayed. Defaults to NULL. If + * no value is specified, the display language codes for every field attached + * to the given entity will be returned. + * @param $langcode + * (optional) The language code $entity has to be displayed in. Defaults to + * NULL. If no value is given the current language will be used. + * + * @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) { + $langcode = $entity->getExistingTranslation($langcode)->language()->id; + $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 ? $langcode : Language::LANGCODE_NOT_SPECIFIED; + } + } + return $display_langcodes; + } + elseif (!empty($definitions[$field_name]['configurable'])) { + return $translatable ? $langcode : Language::LANGCODE_NOT_SPECIFIED; + } +} diff --git a/core/modules/field/field.install b/core/modules/field/field.install index c4211ff..ee6fdca 100644 --- a/core/modules/field/field.install +++ b/core/modules/field/field.install @@ -437,10 +437,7 @@ function field_update_8003() { * @ingroup config_upgrade */ function field_update_8004() { - update_variable_set('field_language_fallback', TRUE); - update_variables_to_config('field.settings', array( - 'field_language_fallback' => 'language_fallback', - )); + // Do nothing: the former update code has been moved to locale_update_8018(). } /** diff --git a/core/modules/field/field.multilingual.inc b/core/modules/field/field.multilingual.inc index 17b065a..63f952c 100644 --- a/core/modules/field/field.multilingual.inc +++ b/core/modules/field/field.multilingual.inc @@ -50,6 +50,7 @@ * - 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. + * * 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 @@ -65,49 +66,6 @@ /** - * Applies language fallback rules to the fields attached to the given entity. - * - * Core language fallback rules simply check if fields have a field translation - * for the requested language code. If so, the requested language is returned, - * otherwise all the fallback candidates are inspected to see if there is a - * field translation available in another language. - * By default this is called by field_field_language_alter(), but this - * behavior can be disabled by setting the 'field.settings.language_fallback' - * variable to FALSE. - * - * @param $field_langcodes - * A reference to an array of language codes keyed by field name. - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to be displayed. - * @param $langcode - * The language code $entity has to be displayed in. - */ -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 (_field_translated_value_exists($entity, $langcode, $field_name)) { - $field_langcodes[$field_name] = $langcode; - } - else { - 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 (_field_translated_value_exists($entity, $fallback_langcode, $field_name)) { - $field_langcodes[$field_name] = $fallback_langcode; - break; - } - } - } - } -} - -/** * Collects the available language codes for the given entity type and field. * * If the given field has language support enabled, an array of available @@ -195,13 +153,6 @@ function field_content_languages() { } /** - * Checks whether field language fallback is enabled. - */ -function field_language_fallback_enabled() { - return language_multilingual() && \Drupal::config('field.settings')->get('language_fallback'); -} - -/** * Checks whether a field has language support. * * A field has language support enabled if its 'translatable' property is set to @@ -241,126 +192,3 @@ function field_has_translation_handler($entity_type, $handler = NULL) { $info = entity_get_info($entity_type); return !empty($info['translatable']); } - -/** - * Ensures that a given language code is valid. - * - * Checks whether the given language code is one of the enabled language codes. - * Otherwise, it returns the current, global language code; or the site's - * default language code, if the additional parameter $default is TRUE. - * - * @param $langcode - * The language code to validate. - * @param $default - * Whether to return the default language code or the current language code in - * case $langcode is invalid. - * - * @return - * A valid language code. - */ -function field_valid_language($langcode, $default = TRUE) { - $languages = field_content_languages(); - if (in_array($langcode, $languages)) { - return $langcode; - } - return $default ? language_default()->id : language(Language::TYPE_CONTENT)->id; -} - -/** - * Returns the display language code for the fields attached to the given - * entity. - * - * The actual language code for each given field is determined based on the - * requested language code and the actual data available in the fields - * themselves. - * If there is no registered translation handler for the given entity type, the - * display language code to be used is just Language::LANGCODE_NOT_SPECIFIED, as no other - * language code is allowed by field_available_languages(). - * - * If translation handlers are found, we let modules provide alternative display - * language codes for fields not having the requested language code available. - * Core language fallback rules are provided by field_language_fallback() - * which is called by field_field_language_alter(). - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to be displayed. - * @param $field_name - * (optional) The name of the field to be displayed. Defaults to NULL. If - * no value is specified, the display language codes for every field attached - * to the given entity will be returned. - * @param $langcode - * (optional) The language code $entity has to be displayed in. Defaults to - * NULL. If no value is given the current language will be used. - * - * @return - * A language code if a field name is specified, an array of language codes - * keyed by field name otherwise. - */ -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 (_field_translated_value_exists($entity, $langcode, $instance['field_name'])) { - $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; - } - } - } - } - - 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; - } - - $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; - } - - return $display_langcode; -} - -/** - * Returns TRUE if a non-empty value exists for a given entity/language/field. - */ -function _field_translated_value_exists(EntityInterface $entity, $langcode, $field_name) { - if (!$entity->hasTranslation($langcode)) { - return FALSE; - } - $field = $entity->getTranslation($langcode)->$field_name; - $field->filterEmptyValues(); - $value = $field->getValue(); - return !empty($value); -} 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 ea74a94..b570ff3 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 @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Language\FallbackManagerInterface; use Drupal\field\Plugin\Type\Formatter\FormatterPluginManager; use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\display\DisplayPluginBase; @@ -78,6 +79,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 @@ -89,10 +97,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; } /** @@ -103,7 +112,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') ); } @@ -233,14 +243,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)); } } @@ -839,11 +842,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->getExistingTranslation($langcode)->language()->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 0edb901..d505194 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']); @@ -252,18 +255,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 = $instance->getField(); @@ -275,30 +274,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']; @@ -306,18 +294,18 @@ 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.settings')->set('fallback', FALSE); $entity->getTranslation($requested_langcode)->{$this->field_name}->value = mt_rand(1, 127); - drupal_static_reset('field_language'); $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 b5704d5..deb7610 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -66,15 +66,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/language.api.php b/core/modules/language/language.api.php index cba3f96..8a8add8 100644 --- a/core/modules/language/language.api.php +++ b/core/modules/language/language.api.php @@ -58,5 +58,77 @@ function hook_language_delete($language) { } /** + * Allow modules to alter the language fallback candidates. + * + * @param array $candidates + * An array of language codes whose order will determine the language fallback + * order. + * @param array $context + * A language fallback context. + * + * @see \Drupal\Core\Language\FallbackHandlerInterface::getCandidates() + */ +function hook_language_fallback_candidates_alter(array &$candidates, array $context) { + $fallback_candidates = array_reverse($fallback_candidates); +} + +/** + * Allow modules to alter the fallback candidates for specific operations. + * + * @param array $candidates + * An array of language codes whose order will determine the language fallback + * order. + * @param array $context + * A language fallback context. + * + * @see \Drupal\Core\Language\FallbackHandlerInterface::getCandidates() + */ +function hook_language_fallback_candidates_OPERATION_alter(array &$candidates, array $context) { + // We know that the current OPERATION deals with entities so no need to check + // here. + if ($context['data']->entityType() == 'node') { + $fallback_candidates = array_reverse($fallback_candidates); + } +} + +/** + * Allow modules to alter the language fallback map. + * + * @param array $map + * A reference to an associative array keyed by the values keys and having the + * language codes to be used as values. + * @param array $context + * A language fallback context. The specified values are available in the + * 'values' key. + * + * @see \Drupal\Core\Language\FallbackHandlerInterface::getValuesMap() + */ +function hook_language_fallback_map_alter(&$map, $context) { + if ($context['data'] instanceof \Drupal\Core\Entity\EntityInterface && $context['data']->hasTranslation('it')) { + $map['field_foo'] = 'it'; + } +} + +/** + * Allow modules to alter the language fallback map for specific operations. + * + * @param array $map + * A reference to an associative array keyed by the values keys and having the + * language codes to be used as values. + * @param array $context + * A language fallback context. The specified values are available in the + * 'values' key. + * + * @see \Drupal\Core\Language\FallbackHandlerInterface::getValuesMap() + */ +function hook_language_fallback_map_OPERATION_alter(&$map, $context) { + // We know that the current OPERATION deals with entities so no need to check + // here. + if ($context['data']->hasTranslation('it')) { + $map['field_foo'] = 'it'; + } +} + +/** * @} End of "addtogroup hooks". */ 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..05d44f3 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/FallbackManager.php @@ -0,0 +1,84 @@ +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 desired language if specified. + 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; + } + + /** + * Lets modules alter the given language fallback data structure. + * + * @param string $type + * A string describing the type of the alterable data. + * @param array $data + * The language fallback data structure to be altered. + * @param array $context + * A language fallback context data structure. + * + * @see \Drupal\Core\Language\FallbackHandlerInterface::getCandidates() + */ + protected function alter($type, array &$data, array $context) { + $types = array(); + if (!empty($context['operation'])) { + $types[] = $type . '_' . $context['operation']; + } + $types[] = $type; + 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..9e34c57 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php @@ -0,0 +1,36 @@ +getDefinition('language_fallback_manager'); + $definition->setClass('Drupal\language\FallbackManager'); + $definition->addArgument(new Reference('module_handler')); + } + +} diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageFallbackTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageFallbackTest.php new file mode 100644 index 0000000..cfec07b --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageFallbackTest.php @@ -0,0 +1,95 @@ + 'Language fallback', + 'description' => 'Tests the language fallback behavior.', + 'group' => 'Language', + ); + } + + /** + * The state storage service. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $state; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->enableModules(array('language', 'language_test')); + $this->installConfig(array('language')); + + $this->state = $this->container->get('state'); + + for ($i = 0; $i < 3; $i++) { + $language = new Language(); + $language->id = $this->randomName(2); + $language->weight = -$i; + language_save($language); + } + } + + /** + * Tests language fallback candidates. + */ + public function testCandidates() { + $manager = $this->getManager(); + $expected = array_keys(language_list() + array(Language::LANGCODE_NOT_SPECIFIED => NULL)); + + // Check that language fallback candidates by default are all the available + // languages sorted by weight. + $candidates = $manager->getCandidates(); + $this->assertEqual(array_values($candidates), $expected, 'Language fallback candidates are properly returned.'); + + // Check that candidates are alterable. + $this->state->set('language_test.fallback_alter.candidates', TRUE); + $expected = array_slice($expected, 0, count($expected) - 1); + $candidates = $manager->getCandidates(); + $this->assertEqual(array_values($candidates), $expected, 'Language fallback candidates are alterable.'); + + // Check that candidates are alterable for specific operations. + $this->state->set('language_test.fallback_alter.candidates', FALSE); + $this->state->set('language_test.fallback_operation_alter.candidates', TRUE); + $expected[] = Language::LANGCODE_NOT_SPECIFIED; + $expected[] = Language::LANGCODE_NOT_APPLICABLE; + $candidates = $manager->getCandidates(array('operation' => 'test')); + $this->assertEqual(array_values($candidates), $expected, 'Language fallback candidates are alterable for specific operations.'); + + // Check that when the Language module is disabled no language fallback is + // applied. + $this->disableModules(array('language', 'language_test')); + $candidates = $this->getManager()->getCandidates(); + $this->assertEqual(array_values($candidates), array(Language::LANGCODE_DEFAULT), 'Language fallback is not applied when the Language module is not enabled.'); + } + + /** + * Returns the language fallback manager service. + * + * @return \Drupal\Core\Language\FallbackManagerInterface + * The language fallback manager. + */ + protected function getManager() { + return $this->container->get('language_fallback_manager'); + } + +} diff --git a/core/modules/language/tests/language_test/language_test.module b/core/modules/language/tests/language_test/language_test.module index 33c2b36..8e8cbaf 100644 --- a/core/modules/language/tests/language_test/language_test.module +++ b/core/modules/language/tests/language_test/language_test.module @@ -109,3 +109,22 @@ function language_test_store_language_negotiation() { function language_test_language_negotiation_method($languages) { return 'it'; } + +/** + * Implements hook_language_fallback_candidates_alter(). + */ +function language_test_language_fallback_candidates_alter(array &$candidates, array $context) { + if (Drupal::state()->get('language_test.fallback_alter.candidates')) { + unset($candidates[Language::LANGCODE_NOT_SPECIFIED]); + } +} + +/** + * Implements hook_language_fallback_candidates_OPERATION_alter(). + */ +function language_test_language_fallback_candidates_test_alter(array &$candidates, array $context) { + if (Drupal::state()->get('language_test.fallback_operation_alter.candidates')) { + $langcode = Language::LANGCODE_NOT_APPLICABLE; + $candidates[$langcode] = $langcode; + } +} diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index d10791c..98a576f 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -971,6 +971,15 @@ function locale_update_8017() { } /** + * Removes the field language fallback settings as it is no longer supported. + * + * @ingroup config_upgrade + */ +function locale_update_8018() { + update_variable_del('locale_field_language_fallback'); +} + +/** * @} 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 1fa6140..f2b9441 100644 --- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php @@ -12,6 +12,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 Drupal\field\FieldInfo; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Cmf\Component\Routing\RouteProviderInterface; @@ -61,8 +63,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, FieldInfo $field_info, UuidInterface $uuid_service, RouteProviderInterface $route_provider) { - parent::__construct($entity_type, $entity_info, $database, $field_info, $uuid_service); + public function __construct($entity_type, array $entity_info, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service, LanguageManager $language_manager, FallbackManagerInterface $fallback_manager, RouteProviderInterface $route_provider) { + parent::__construct($entity_type, $entity_info, $database, $field_info, $uuid_service, $language_manager, $fallback_manager); $this->routeProvider = $route_provider; @@ -93,6 +95,8 @@ public static function createInstance(ContainerInterface $container, $entity_typ $container->get('database'), $container->get('field.info'), $container->get('uuid'), + $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 77c0bbd..138c0c1 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php @@ -79,7 +79,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(); @@ -111,7 +111,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.tokens.inc b/core/modules/node/node.tokens.inc index 298d44e..f2c888d 100644 --- a/core/modules/node/node.tokens.inc +++ b/core/modules/node/node.tokens.inc @@ -137,11 +137,11 @@ function node_tokens($type, $tokens, array $data = array(), array $options = arr case 'body': case 'summary': - if (($items = $node->getTranslation($langcode)->get('body')) && !$items->isEmpty()) { + $translation = $node->getExistingTranslation($langcode); + if (($items = $translation->get('body')) && !$items->isEmpty()) { $item = $items[0]; $instance = field_info_instance('node', 'body', $node->getType()); - $field_langcode = field_language($node, 'body', $langcode); - + $field_langcode = $translation->language()->id; // If the summary was requested and is not empty, use it. if ($name == 'summary' && !empty($item->summary)) { $output = $sanitize ? $item->summary_processed : $item->summary; diff --git a/core/modules/system/language.api.php b/core/modules/system/language.api.php index cfa42ec..de43f83 100644 --- a/core/modules/system/language.api.php +++ b/core/modules/system/language.api.php @@ -152,17 +152,6 @@ function hook_language_negotiation_info_alter(array &$negotiation_info) { } /** - * Perform alterations on the language fallback candidates. - * - * @param $fallback_candidates - * An array of language codes whose order will determine the language fallback - * order. - */ -function hook_language_fallback_candidates_alter(array &$fallback_candidates) { - $fallback_candidates = array_reverse($fallback_candidates); -} - -/** * @} End of "addtogroup hooks". */ 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 d6fb956..2a6ba46 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -81,6 +81,7 @@ function setUp() { $language = new Language(array( 'id' => 'l' . $i, 'name' => $this->randomString(), + 'weight' => $i, )); $this->langcodes[$i] = $language->id; language_save($language); @@ -491,4 +492,54 @@ function testEntityTranslationAPI() { $this->assertEqual($field->getLangcode(), $langcode2, 'Field object has the expected langcode.'); } + /** + * 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->getExistingTranslation($langcode2); + $this->assertEqual($translation->language()->id, $default_langcode, 'The current translation language matches the expected one.'); + + // Check that language fallback respects language weight by default. + $languages = language_list(); + $languages[$langcode]->weight = -1; + language_save($languages[$langcode]); + $translation = $entity->getExistingTranslation($langcode2); + $this->assertEqual($translation->language()->id, $langcode, 'The current translation language matches the expected one.'); + + // Check that the current translation is properly returned. + $translation = $entity->getCurrentTranslation(); + $this->assertEqual($langcode, $translation->language()->id, 'The current translation language matches the topmost language fallback candidate.'); + $current_langcode = $this->container->get('language_manager')->getLanguage(Language::TYPE_CONTENT)->id; + $entity->addTranslation($current_langcode); + $translation = $entity->getCurrentTranslation(); + $this->assertEqual($current_langcode, $translation->language()->id, 'The current translation language matches the current language.'); + + // Check that if the entity has no translation no fallback is applied. + $entity = $this->entityManager + ->getStorageController('entity_test_mul') + ->create(array('langcode' => $default_langcode)); + $translation = $entity->getExistingTranslation($default_langcode); + $this->assertIdentical($entity, $translation, 'When the entity has no translation no fallback is applied.'); + } + } diff --git a/core/modules/user/lib/Drupal/user/UserStorageController.php b/core/modules/user/lib/Drupal/user/UserStorageController.php index 03d32b1..9dad72e 100644 --- a/core/modules/user/lib/Drupal/user/UserStorageController.php +++ b/core/modules/user/lib/Drupal/user/UserStorageController.php @@ -8,6 +8,8 @@ namespace Drupal\user; use Drupal\Component\Uuid\UuidInterface; +use Drupal\Core\Language\FallbackManagerInterface; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Password\PasswordInterface; use Drupal\Core\Database\Connection; @@ -56,8 +58,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, FieldInfo $field_info, UuidInterface $uuid_service, PasswordInterface $password, UserDataInterface $user_data) { - parent::__construct($entity_type, $entity_info, $database, $field_info, $uuid_service); + public function __construct($entity_type, $entity_info, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service, LanguageManager $languageManager, FallbackManagerInterface $fallback_manager, PasswordInterface $password, UserDataInterface $user_data) { + parent::__construct($entity_type, $entity_info, $database, $field_info, $uuid_service, $languageManager, $fallback_manager); $this->password = $password; $this->userData = $user_data; @@ -73,6 +75,8 @@ public static function createInstance(ContainerInterface $container, $entity_typ $container->get('database'), $container->get('field.info'), $container->get('uuid'), + $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 7d88791..75dd90c 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) { @@ -922,11 +938,24 @@ public function getExportProperties() { * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation(). */ public function getTranslation($langcode) { - // @todo Revisit this once config entities are converted to NG. return $this; } /** + * {@inheritdoc} + */ + public function getExistingTranslation($langcode, $context = array()) { + return $this->storage->getExistingTranslation($langcode, $context); + } + + /** + * {@inheritdoc} + */ + public function getCurrentTranslation() { + return $this->storage->getCurrentTranslation(); + } + + /** * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages(). */ public function getTranslationLanguages($include_default = TRUE) {