diff --git a/core/core.services.yml b/core/core.services.yml index a827835..0d3de89 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -213,6 +213,9 @@ services: language_manager: class: Drupal\Core\Language\LanguageManager arguments: ['@state'] + language_fallback_mapper: + class: Drupal\Core\Language\NoLanguageFallbackMapper + arguments: ['@language_manager'] string_translator.custom_strings: class: Drupal\Core\StringTranslation\Translator\CustomStrings arguments: ['@settings'] diff --git a/core/includes/language.inc b/core/includes/language.inc index e057616..8e24c6e 100644 --- a/core/includes/language.inc +++ b/core/includes/language.inc @@ -553,20 +553,14 @@ function language_url_split_prefix($path, $languages) { * * @return * An array of language codes. + * + * @see \Drupal\Core\Language\LanguageFallbackMapperInterface::getCandidateLangcodes() + * + * @deprecated This has been deprecated in favor of the language fallback + * mapper. */ 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_mapper')->getCandidateLangcodes(array('type' => $type)); } /** diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 177a495..d7041ae 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -423,6 +423,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; + } } /** @@ -658,6 +664,7 @@ protected function initializeTranslation($langcode) { $translation->values = &$this->values; $translation->fields = &$this->fields; $translation->translations = &$this->translations; + $translation->enforceIsNew = &$this->enforceIsNew; $translation->translationInitialize = FALSE; return $translation; @@ -666,6 +673,38 @@ protected function initializeTranslation($langcode) { /** * {@inheritdoc} */ + public function getTranslationFromContext($langcode = NULL, $context = array()) { + $translation = $this; + + if (empty($langcode)) { + $langcode = \Drupal::languageManager()->getLanguage(Language::TYPE_CONTENT)->id; + } + + // Retrieve language fallback candidates to perform the entity language + // negotiation. + $context['langcode'] = $langcode; + $context['data'] = $this; + $context += array('operation' => 'entity_view'); + $candidates = \Drupal::service('language_fallback_mapper')->getCandidateLangcodes($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 hasTranslation($langcode) { $default_language = $this->language ?: $this->getDefaultLanguage(); if ($langcode == $default_language->id) { diff --git a/core/lib/Drupal/Core/Entity/ContentEntityFormController.php b/core/lib/Drupal/Core/Entity/ContentEntityFormController.php index 2103fc2..017e4f1 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityFormController.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityFormController.php @@ -83,43 +83,22 @@ protected function init(array &$form_state) { * @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); - $translation = $this->entity->getTranslation($langcode); - // Ensure that the entity object is a BC entity if the original one is. - return $this->entity instanceof EntityBCDecorator ? $translation->getBCEntity() : $translation; + return $this->entity->getTranslation($langcode); } /** * {@inheritdoc} */ - 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(); - $languageManager = \Drupal::languageManager(); - $langcode = $languageManager->getLanguage(Language::TYPE_CONTENT)->id; - $fallback = $languageManager->isMultilingual() ? 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. - if (!empty($langcode)) { - return $langcode; - } - else { - // If the entity is translatable, return the original language. - return $entity->getUntranslated()->language()->id; + public function getFormLangcode(array &$form_state) { + if (empty($form_state['langcode'])) { + // Imply a 'view' operation to ensure users edit entities in the same + // language they are displayed. This allows to keep contextual editing + // working also for multilingual entities. + $form_state['langcode'] = $this->entity->getTranslationFromContext()->language()->id; } + return $form_state['langcode']; } /** @@ -138,6 +117,8 @@ public function buildEntity(array $form, array &$form_state) { $info = entity_get_info($entity_type); // @todo Exploit the Field API to process the submitted entity fields. + $this->updateFormLangcode($form_state); + // Copy top-level form values that are entity fields but not handled by // field API without changing existing entity fields that are not being // edited by this form. Values of fields handled by field API are copied diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index 52ac7d5..7e911c1 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -325,7 +325,7 @@ public function delete(array $form, array &$form_state) { /** * {@inheritdoc} */ - public function getFormLangcode(array $form_state) { + public function getFormLangcode(array &$form_state) { return $this->entity->language()->id; } 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/EntityRenderController.php b/core/lib/Drupal/Core/Entity/EntityRenderController.php index 9d68684..5970323 100644 --- a/core/lib/Drupal/Core/Entity/EntityRenderController.php +++ b/core/lib/Drupal/Core/Entity/EntityRenderController.php @@ -167,9 +167,14 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la $view_modes = array(); $displays = array(); $context = array('langcode' => $langcode); - foreach ($entities as $entity) { + foreach ($entities as $key => $entity) { $bundle = $entity->bundle(); + // Ensure that from now on we are dealing with the proper translation + // object. + $entity = $entity->getTranslationFromContext($langcode); + $entities[$key] = $entity; + // Allow modules to change the view mode. $entity_view_mode = $view_mode; drupal_alter('entity_view_mode', $entity_view_mode, $entity, $context); diff --git a/core/lib/Drupal/Core/Language/Language.php b/core/lib/Drupal/Core/Language/Language.php index 7181b32..b5ea710 100644 --- a/core/lib/Drupal/Core/Language/Language.php +++ b/core/lib/Drupal/Core/Language/Language.php @@ -153,7 +153,7 @@ public function extend($obj) { * @param array $languages * The array of language objects keyed by langcode. */ - public static function sort($languages) { + public static function sort(&$languages) { uasort($languages, function ($a, $b) { $a_weight = isset($a->weight) ? $a->weight : 0; $b_weight = isset($b->weight) ? $b->weight : 0; diff --git a/core/lib/Drupal/Core/Language/LanguageFallbackMapperInterface.php b/core/lib/Drupal/Core/Language/LanguageFallbackMapperInterface.php new file mode 100644 index 0000000..d1a1b2c --- /dev/null +++ b/core/lib/Drupal/Core/Language/LanguageFallbackMapperInterface.php @@ -0,0 +1,33 @@ +languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function getCandidateLangcodes(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 f94aa0b..e4e7dbe 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\LanguageFallbackMapperInterface; + /** * Interface for translatable data. */ @@ -35,10 +38,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 +51,26 @@ 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 $langcode + * (optional) The language of the current context. Defaults to the current + * content language. + * @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\LanguageFallbackMapperInterface + */ + public function getTranslationFromContext($langcode = NULL, $context = array()); /** * 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 efb0e8e..7159bc1 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentFormController.php +++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php @@ -58,7 +58,6 @@ public static function create(ContainerInterface $container) { * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. */ - public function __construct(EntityManager $entity_manager, FieldInfo $field_info, AccountInterface $current_user) { $this->entityManager = $entity_manager; $this->fieldInfo = $field_info; @@ -66,6 +65,22 @@ public function __construct(EntityManager $entity_manager, FieldInfo $field_info } /** + * {@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 = \Drupal::languageManager()->getLanguage(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) { @@ -207,13 +222,6 @@ public function form(array $form, array &$form_state) { '#value' => ($comment->id() ? !$comment->uid->target_id : $this->currentUser->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', 'entity_id', 'entity_type', 'field_id', 'uid', 'langcode') as $key) { diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 661aac9..2598bb6 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -313,7 +313,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); } /** @@ -617,7 +622,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 instanceof ContentEntityInterface && $entity->isTranslatable()) { + $form_controller = content_translation_form_controller($form_state); + $entity = $form_controller ? $form_controller->getEntity() : NULL; + + if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && count($entity->getTranslationLanguages()) > 1) { $controller = content_translation_controller($entity->entityType()); $controller->entityFormAlter($form, $form_state, $entity); @@ -640,28 +648,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 ($entity instanceof ContentEntityInterface && 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 21c8465..c24e064 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,18 +34,18 @@ * Tests the basic translation UI. */ function testTranslationUI() { - $this->assertBasicTranslation(); + $this->doTestBasicTranslation(); $this->doTestTranslationOverview(); - $this->assertOutdatedStatus(); - $this->assertPublishedStatus(); - $this->assertAuthoringInfo(); - $this->assertTranslationDeletion(); + $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); @@ -117,7 +117,7 @@ protected function doTestTranslationOverview() { /** * 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]; @@ -150,7 +150,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); @@ -172,7 +172,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(); @@ -194,8 +194,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. @@ -207,14 +207,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 0a9ebdf..b6172c1 100644 --- a/core/modules/field/config/field.settings.yml +++ b/core/modules/field/config/field.settings.yml @@ -1,2 +1 @@ -language_fallback: true 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 9996784..424132b 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 b4f0198..c252e2e 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.attach.inc b/core/modules/field/field.attach.inc index 548cc74..27de865 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -120,21 +120,23 @@ function field_invoke_method($method, $target_function, EntityInterface $entity, $langcodes = _field_language_suggestion($available_langcodes, $options['langcode'], $field_name); foreach ($langcodes as $langcode) { - $items = $entity->getTranslation($langcode)->get($field_name); - $items->filterEmptyValues(); + if ($entity->hasTranslation($langcode)) { + $items = $entity->getTranslation($langcode)->get($field_name); + $items->filterEmptyValues(); - $result = $target->$method($items, $a, $b); + $result = $target->$method($items, $a, $b); - if (isset($result)) { - // For methods with array results, we merge results together. - // For methods with scalar results, we collect results in an array. - if (is_array($result)) { - $return = array_merge($return, $result); + if (isset($result)) { + // For methods with array results, we merge results together. + // For methods with scalar results, we collect results in an array. + if (is_array($result)) { + $return = array_merge($return, $result); + } + else { + $return[] = $result; + } } - else { - $return[] = $result; } - } } } } @@ -223,10 +225,12 @@ function field_invoke_method_multiple($method, $target_function, array $entities $langcode = !empty($options['langcode'][$id]) ? $options['langcode'][$id] : $options['langcode']; $langcodes = _field_language_suggestion($available_langcodes, $langcode, $field_name); foreach ($langcodes as $langcode) { - // Group the items corresponding to the current field. - $items = $entity->getTranslation($langcode)->get($field_name); - $items->filterEmptyValues(); - $grouped_items[$instance_uuid][$langcode][$id] = $items; + if ($entity->hasTranslation($langcode)) { + // Group the items corresponding to the current field. + $items = $entity->getTranslation($langcode)->get($field_name); + $items->filterEmptyValues(); + $grouped_items[$instance_uuid][$langcode][$id] = $items; + } } } } diff --git a/core/modules/field/field.deprecated.inc b/core/modules/field/field.deprecated.inc index db6f339..b4c104a 100644 --- a/core/modules/field/field.deprecated.inc +++ b/core/modules/field/field.deprecated.inc @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Language\Language; use Drupal\entity\Entity\EntityDisplay; use Drupal\field\Field; @@ -856,3 +857,92 @@ function field_access($op, FieldInterface $field, $entity_type, $entity = NULL, $items = $entity ? $entity->get($field->id()) : NULL; return $access_controller->fieldAccess($op, $field, $account, $items); } + +/** + * Checks whether field language fallback is enabled. + * + * @see \Drupal\Core\Language\LanguageFallbackMapperInterface + * + * @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_mapper')->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\LanguageFallbackMapperInterface::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->getTranslationFromContext($langcode)->language()->id; + $definitions = $entity->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 2da4aec..55d6ff4 100644 --- a/core/modules/field/field.install +++ b/core/modules/field/field.install @@ -442,10 +442,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 96ecabb..68493c7 100644 --- a/core/modules/field/field.multilingual.inc +++ b/core/modules/field/field.multilingual.inc @@ -66,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 @@ -196,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 @@ -242,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, $displayed_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 $field_name => $instance) { - if (_field_translated_value_exists($entity, $langcode, $field_name)) { - $display_langcode[$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[$field_name] = Language::LANGCODE_NOT_SPECIFIED; - foreach (language_list(Language::STATE_LOCKED) as $language_locked) { - if (isset($entity->{$field_name}[$language_locked->id])) { - $display_langcode[$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($displayed_field_name)) { - return isset($display_langcode[$displayed_field_name]) ? $display_langcode[$displayed_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 995bc9e..b57d152 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 @@ -14,6 +14,7 @@ use Drupal\Core\Field\FormatterPluginManager; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Language\LanguageFallbackMapperInterface; use Drupal\views\Views; use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\display\DisplayPluginBase; @@ -93,6 +94,13 @@ class Field extends FieldPluginBase { protected $languageManager; /** + * The language fallback manager. + * + * @var \Drupal\Core\Language\LanguageFallbackMapperInterface; + */ + protected $languageFallbackMapper; + + /** * Constructs a \Drupal\field\Plugin\views\field\Field object. * * @param array $configuration @@ -107,13 +115,16 @@ class Field extends FieldPluginBase { * The field formatter plugin manager. * @param \Drupal\Core\Language\LanguageManager $language_manager * The language manager. + * @param \Drupal\Core\Language\LanguageFallbackMapperInterface $language_fallback_mapper + * The language fallback mapper. */ - public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManager $entity_manager, FormatterPluginManager $formatter_plugin_manager, LanguageManager $language_manager) { + public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManager $entity_manager, FormatterPluginManager $formatter_plugin_manager, LanguageManager $language_manager, LanguageFallbackMapperInterface $language_fallback_mapper) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->entityManager = $entity_manager; $this->formatterPluginManager = $formatter_plugin_manager; $this->languageManager = $language_manager; + $this->languageFallbackMapper = $language_fallback_mapper; } /** @@ -126,7 +137,8 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $container->get('entity.manager'), $container->get('plugin.manager.field.formatter'), - $container->get('language_manager') + $container->get('language_manager'), + $container->get('language_fallback_mapper') ); } @@ -260,14 +272,7 @@ public function query($use_groupby = FALSE) { $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->languageFallbackMapper->getCandidateLangcodes(array('operation' => 'views_query', 'langcode' => $langcode, 'data' => $this)); $this->query->addWhereExpression(0, "$column IN($placeholder) OR $column IS NULL", array($placeholder => $langcode_fallback_candidates)); } } @@ -866,11 +871,12 @@ function field_langcode(EntityInterface $entity) { $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. + $langcode = $entity->getTranslationFromContext($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 b381cf5..de2dd94 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'; @@ -159,6 +161,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->getFieldCardinality()); @@ -228,100 +231,4 @@ function testTranslatableFieldSaveLoad() { } } - /** - * Tests display language logic for translatable fields. - */ - function testFieldDisplayLanguage() { - $field_name = drupal_strtolower($this->randomName() . '_field_name'); - $entity_type = 'entity_test'; - $bundle = 'entity_test'; - - // We need an additional field here to properly test display language - // suggestions. - $field = array( - 'name' => $field_name, - 'entity_type' => $entity_type, - 'type' => 'test_field', - 'cardinality' => 2, - 'translatable' => TRUE, - ); - entity_create('field_entity', $field)->save(); - - $instance = array( - 'field_name' => $field['name'], - 'entity_type' => $entity_type, - 'bundle' => $bundle, - ); - entity_create('field_instance', $instance)->save(); - - $enabled_langcodes = field_content_languages(); - $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, $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. - foreach ($instances as $instance) { - $field_name = $instance->getFieldName(); - $field = $instance->getField(); - do { - // Index 0 is reserved for the requested language, this way we ensure - // that no field is actually populated with it. - $langcode = $enabled_langcodes[mt_rand(1, count($enabled_langcodes) - 1)]; - } - while (isset($langcodes[$langcode])); - $langcodes[$langcode] = TRUE; - $entity->getTranslation($langcode)->{$field_name}->setValue($this->_generateTestFieldValues($field->getFieldCardinality())); - // 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->getFieldName(); - $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]))); - } - - // 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->getFieldName(); - $langcode = $display_langcodes[$field_name]; - // 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))); - } - - // 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))); - - // Test field_language() basic behavior without language fallback. - \Drupal::state()->set('field_test.language_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/lib/Drupal/field/Tests/TranslationWebTest.php b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php index 8ddb73a..ea15aeb 100644 --- a/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php @@ -104,6 +104,7 @@ function testFieldFormTranslationRevisions() { field_test_entity_info_translatable($this->entity_type, TRUE); $entity = entity_create($this->entity_type, array()); $available_langcodes = array_flip(field_available_languages($this->entity_type, $this->field)); + ksort($available_langcodes); unset($available_langcodes[Language::LANGCODE_NOT_SPECIFIED]); unset($available_langcodes[Language::LANGCODE_NOT_APPLICABLE]); $field_name = $this->field->getFieldName(); 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 992e6ef..c02a40c 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -63,15 +63,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..9ca2684 100644 --- a/core/modules/language/language.api.php +++ b/core/modules/language/language.api.php @@ -58,5 +58,39 @@ 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\LanguageFallbackMapperInterface::getCandidateLangcodes() + */ +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\LanguageFallbackMapperInterface::getCandidateLangcodes() + */ +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); + } +} + +/** * @} End of "addtogroup hooks". */ diff --git a/core/modules/language/lib/Drupal/language/LanguageFallbackMapper.php b/core/modules/language/lib/Drupal/language/LanguageFallbackMapper.php new file mode 100644 index 0000000..d46c71e --- /dev/null +++ b/core/modules/language/lib/Drupal/language/LanguageFallbackMapper.php @@ -0,0 +1,84 @@ +moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function getCandidateLangcodes(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\LanguageFallbackMapperInterface::getCandidateLangcodes() + */ + 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..72f8055 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php @@ -0,0 +1,36 @@ +getDefinition('language_fallback_mapper') + ->setClass('Drupal\language\LanguageFallbackMapper') + ->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..2f3deeb --- /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() { + $mapper = $this->getMapper(); + $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 = $mapper->getCandidateLangcodes(); + $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 = $mapper->getCandidateLangcodes(); + $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 = $mapper->getCandidateLangcodes(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->getMapper()->getCandidateLangcodes(); + $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 mapper service. + * + * @return \Drupal\Core\Language\LanguageFallbackMapperInterface + * The language fallback mapper. + */ + protected function getMapper() { + return $this->container->get('language_fallback_mapper'); + } + +} 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 3ab0247..c55e203 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -965,6 +965,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/node/lib/Drupal/node/Controller/NodeController.php b/core/modules/node/lib/Drupal/node/Controller/NodeController.php index c889adc..e8b38c7 100644 --- a/core/modules/node/lib/Drupal/node/Controller/NodeController.php +++ b/core/modules/node/lib/Drupal/node/Controller/NodeController.php @@ -126,7 +126,7 @@ public function page(NodeInterface $node) { * The page title. */ public function pageTitle(NodeInterface $node) { - return String::checkPlain($node->label()); + return String::checkPlain($node->getTranslationFromContext()->label()); } /** diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php index 6a2608c..baa8d23 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php @@ -72,7 +72,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(); @@ -104,7 +104,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(); @@ -188,4 +188,47 @@ public function testDisabledBundle() { $this->assertEqual($enabledNode->id(), reset($rows)->entity_id); } + /** + * Tests that translations are rendered properly. + */ + function testTranslationRendering() { + $default_langcode = $this->langcodes[0]; + $values[$default_langcode] = $this->getNewEntityValues($default_langcode); + $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode); + $node = \Drupal::entityManager()->getStorageController($this->entityType)->load($this->entityId); + $node->setPromoted(TRUE); + + // Create translations. + foreach (array_diff($this->langcodes, array($default_langcode)) as $langcode) { + $values[$langcode] = $this->getNewEntityValues($langcode); + $translation = $node->addTranslation($langcode, $values[$langcode]); + $translation->setPromoted(TRUE); + } + $node->save(); + + // Test that the frontpage view displays the correct translations. + \Drupal::moduleHandler()->install(array('views'), TRUE); + $this->rebuildContainer(); + $this->doTestTranslations('node', $values); + + // Test that the node page displays the correct translations. + $this->doTestTranslations('node/' . $node->id(), $values); + } + + /** + * Tests that the given path dsiplays the correct translation values. + * + * @param string $path + * The path to be tested. + * @param array $values + * The translation values to be found. + */ + protected function doTestTranslations($path, array $values) { + $languages = language_list(); + foreach ($this->langcodes as $langcode) { + $this->drupalGet($path, array('language' => $languages[$langcode])); + $this->assertText($values[$langcode]['title'], format_string('The %langcode node translation is correctly displayed.', array('%langcode' => $langcode))); + } + } + } diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc index 2f5d821..c4b4452 100644 --- a/core/modules/node/node.tokens.inc +++ b/core/modules/node/node.tokens.inc @@ -129,11 +129,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->getTranslationFromContext($langcode, array('operation' => 'node_tokens')); + 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 e2b0d4f..2094c1f 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -10,6 +10,7 @@ use Drupal\Core\Language\Language; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\entity_test\Entity\EntityTestMulRev; +use Drupal\Component\Utility\MapArray; /** * Tests entity translation. @@ -83,6 +84,7 @@ function setUp() { $language = new Language(array( 'id' => 'l' . $i, 'name' => $this->randomString(), + 'weight' => $i, )); $this->langcodes[$i] = $language->id; language_save($language); @@ -494,6 +496,69 @@ function testEntityTranslationAPI() { } /** + * Tests language fallback applied to field and entity translations. + */ + function testLanguageFallback() { + $current_langcode = $this->container->get('language_manager')->getLanguage(Language::TYPE_CONTENT)->id; + $this->langcodes[] = $current_langcode; + + $values = array(); + foreach ($this->langcodes as $langcode) { + $values[$langcode]['name'] = $this->randomName(); + $values[$langcode]['user_id'] = mt_rand(0, 127); + } + + $default_langcode = $this->langcodes[0]; + $langcode = $this->langcodes[1]; + $langcode2 = $this->langcodes[2]; + + $entity_type = 'entity_test_mul'; + $controller = $this->entityManager->getStorageController($entity_type); + $entity = $controller->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->getTranslationFromContext($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->getTranslationFromContext($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->getTranslationFromContext(); + $this->assertEqual($langcode, $translation->language()->id, 'The current translation language matches the topmost language fallback candidate.'); + $entity->addTranslation($current_langcode, $values[$current_langcode]); + $translation = $entity->getTranslationFromContext(); + $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. + $entity2 = $controller->create(array('langcode' => $default_langcode)); + $translation = $entity2->getTranslationFromContext($default_langcode); + $this->assertIdentical($entity2, $translation, 'When the entity has no translation no fallback is applied.'); + + // Checks that entity translations are rendered properly. + $controller = $this->entityManager->getRenderController($entity_type); + $build = $controller->view($entity); + $this->assertEqual($build['label']['#markup'], $values[$current_langcode]['name'], 'By default the entity is rendered in the current language.'); + $langcodes = MapArray::copyValuesToKeys($this->langcodes); + // We have no translation for the $langcode2 langauge, hence the expected + // result is the topmost existing translation, that is $langcode. + $langcodes[$langcode2] = $langcode; + foreach ($langcodes as $desired => $expected) { + $build = $controller->view($entity, 'full', $desired); + $this->assertEqual($build['label']['#markup'], $values[$expected]['name'], 'The entity is rendered in the expected language.'); + } + } + + /** * Check that field translatability is handled properly. */ function testFieldDefinitions() { diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php index bb9617b..0d24ea8 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTest.php @@ -92,8 +92,11 @@ protected function init() { /** * Overrides Drupal\entity\Entity::label(). */ - public function label($langcode = Language::LANGCODE_DEFAULT) { + public function label($langcode = NULL) { $info = $this->entityInfo(); + if (!isset($langcode)) { + $langcode = $this->activeLangcode; + } if (isset($info['entity_keys']['label']) && $info['entity_keys']['label'] == 'name') { return $this->getTranslation($langcode)->name->value; } diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMul.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMul.php index 76db7ab..8dfca6f 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMul.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Entity/EntityTestMul.php @@ -20,6 +20,7 @@ * module = "entity_test", * controllers = { * "storage" = "Drupal\entity_test\EntityTestStorageController", + * "render" = "Drupal\entity_test\EntityTestRenderController", * "access" = "Drupal\entity_test\EntityTestAccessController", * "form" = { * "default" = "Drupal\entity_test\EntityTestFormController"