diff --git a/core/core.services.yml b/core/core.services.yml index 882cf9f..b1e5905 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -218,6 +218,8 @@ services: language_manager: class: Drupal\Core\Language\LanguageManager arguments: ['@state'] + language_fallback_mapper: + class: Drupal\Core\Language\NoFallbackMapper 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..07cc383 100644 --- a/core/includes/language.inc +++ b/core/includes/language.inc @@ -546,29 +546,5 @@ function language_url_split_prefix($path, $languages) { } /** - * Returns the possible fallback languages ordered by language weight. - * - * @param - * (optional) The language type. Defaults to Language::TYPE_CONTENT. - * - * @return - * An array of language codes. - */ -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; -} - -/** * @} End of "language_negotiation" */ diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 177a495..5447e90 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; diff --git a/core/lib/Drupal/Core/Entity/ContentEntityFormController.php b/core/lib/Drupal/Core/Entity/ContentEntityFormController.php index 2103fc2..859caec 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityFormController.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityFormController.php @@ -17,6 +17,13 @@ class ContentEntityFormController extends EntityFormController { /** + * The entity being used by this form. + * + * @var \Drupal\Core\Entity\EntityManager + */ + protected $entityManager; + + /** * {@inheritdoc} */ public function form(array $form, array &$form_state) { @@ -38,6 +45,7 @@ public function form(array $form, array &$form_state) { * {@inheritdoc} */ public function validate(array $form, array &$form_state) { + $this->updateFormLangcode($form_state); $entity = $this->buildEntity($form, $form_state); $entity_type = $entity->entityType(); $entity_langcode = $entity->language()->id; @@ -73,53 +81,22 @@ public function validate(array $form, array &$form_state) { protected function init(array &$form_state) { // Ensure we act on the translation object corresponding to the current form // language. - $this->entity = $this->getTranslatedEntity($form_state); - parent::init($form_state); - } - - /** - * Returns the translation object corresponding to the form language. - * - * @param array $form_state - * A keyed array containing the current state of the form. - */ - 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; + $this->entity = $this->entity->getTranslation($langcode); + parent::init($form_state); } /** * {@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->entityManager()->getTranslationFromContext($this->entity)->language()->id; } + return $form_state['langcode']; } /** @@ -136,8 +113,8 @@ public function buildEntity(array $form, array &$form_state) { $entity = clone $this->entity; $entity_type = $entity->entityType(); $info = entity_get_info($entity_type); - // @todo Exploit the Field API to process the submitted entity fields. + // @todo Exploit the Entity Field API to process the submitted field values. // 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 @@ -163,4 +140,18 @@ public function buildEntity(array $form, array &$form_state) { } return $entity; } + + /** + * Gets the entity manager. + * + * @return \Drupal\Core\Entity\EntityManager + * The entity manager. + */ + protected function entityManager() { + if (!$this->entityManager) { + $this->entityManager = $this->container()->get('entity.manager'); + } + return $this->entityManager; + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index 52ac7d5..54ee489 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -269,6 +269,7 @@ protected function actions(array $form, array &$form_state) { * {@inheritdoc} */ public function validate(array $form, array &$form_state) { + $this->updateFormLangcode($form_state); // @todo Remove this. // Execute legacy global validation handlers. unset($form_state['validate_handlers']); @@ -292,8 +293,6 @@ public function validate(array $form, array &$form_state) { public function submit(array $form, array &$form_state) { // Remove button and internal Form API values from submitted values. form_state_values_clean($form_state); - - $this->updateFormLangcode($form_state); $this->entity = $this->buildEntity($form, $form_state); return $this->entity; } @@ -325,7 +324,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/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 9417884..0b32bb2 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -625,4 +625,51 @@ public function getEntityTypeLabels() { return $options; } + /** + * Returns the entity 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\Entity\ContentEntityInterface + * A content entity object for the translated data. + * + * @see \Drupal\Core\Language\LanguageFallbackMapperInterface + */ + function getTranslationFromContext(ContentEntityInterface $entity, $langcode = NULL, $context = array()) { + $translation = $entity; + + if (empty($langcode)) { + $langcode = $this->languageManager->getLanguage(Language::TYPE_CONTENT)->id; + } + + // Retrieve language fallback candidates to perform the entity language + // negotiation. + $context['data'] = $entity; + $context += array('operation' => 'entity_view'); + $candidates = \Drupal::service('language_fallback_mapper')->getCandidateLangcodes($langcode, $context); + + // Ensure the default language has the proper language code. + $default_language = $entity->getUntranslated()->language(); + $candidates[$default_language->id] = Language::LANGCODE_DEFAULT; + + // Return the most fitting entity translation. + foreach ($candidates as $candidate) { + if ($entity->hasTranslation($candidate)) { + $translation = $entity->getTranslation($candidate); + break; + } + } + + return $translation; + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index b79ca59..b073795 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -7,13 +7,15 @@ namespace Drupal\Core\Entity; -use Drupal\entity\Entity\EntityDisplay; +use Drupal\Core\Entity\EntityManager; use Drupal\Core\Language\Language; +use Drupal\entity\Entity\EntityDisplay; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base class for entity view controllers. */ -class EntityViewBuilder implements EntityViewBuilderInterface { +class EntityViewBuilder implements EntityControllerInterface, EntityViewBuilderInterface { /** * The type of entities for which this controller is instantiated. @@ -32,6 +34,13 @@ class EntityViewBuilder implements EntityViewBuilderInterface { protected $entityInfo; /** + * The entity manager service. + * + * @var \Drupal\Core\Entity\EntityManager + */ + protected $entityManager; + + /** * An array of view mode info for the type of entities for which this * controller is instantiated. * @@ -49,15 +58,33 @@ class EntityViewBuilder implements EntityViewBuilderInterface { */ protected $cacheBin = 'cache'; - public function __construct($entity_type) { + /** + * Constructs a new EntityViewBuilder. + * + * @param string $entity_type + * The entity type. + * @param array $entity_info + * The entity information array. + * @param \Drupal\Core\Entity\EntityManager $entity_manager + * The entity manager service. + */ + public function __construct($entity_type, array $entity_info, EntityManager $entity_manager) { $this->entityType = $entity_type; - $this->entityInfo = entity_get_info($entity_type); + $this->entityInfo = $entity_info; + $this->entityManager = $entity_manager; $this->viewModesInfo = entity_get_view_modes($entity_type); } /** * {@inheritdoc} */ + public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { + return new static($entity_type, $entity_info, $container->get('entity.manager')); + } + + /** + * {@inheritdoc} + */ public function buildContent(array $entities, array $displays, $view_mode, $langcode = NULL) { field_attach_prepare_view($this->entityType, $entities, $displays, $langcode); @@ -167,9 +194,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 = $this->entityManager->getTranslationFromContext($entity, $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..d14d527 --- /dev/null +++ b/core/lib/Drupal/Core/Language/LanguageFallbackMapperInterface.php @@ -0,0 +1,35 @@ +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/comment/lib/Drupal/comment/CommentViewBuilder.php b/core/modules/comment/lib/Drupal/comment/CommentViewBuilder.php index 70fc2ef..0d001b4 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentViewBuilder.php +++ b/core/modules/comment/lib/Drupal/comment/CommentViewBuilder.php @@ -24,13 +24,6 @@ class CommentViewBuilder extends EntityViewBuilder implements EntityViewBuilderInterface, EntityControllerInterface { /** - * The entity manager service. - * - * @var \Drupal\Core\Entity\EntityManager - */ - protected $entityManager; - - /** * The field info service. * * @var \Drupal\field\FieldInfo @@ -57,6 +50,7 @@ class CommentViewBuilder extends EntityViewBuilder implements EntityViewBuilderI public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { return new static( $entity_type, + $entity_info, $container->get('entity.manager'), $container->get('field.info'), $container->get('module_handler'), @@ -69,6 +63,8 @@ public static function createInstance(ContainerInterface $container, $entity_typ * * @param string $entity_type * The entity type. + * @param array $entity_info + * The entity information array. * @param \Drupal\Core\Entity\EntityManager $entity_manager * The entity manager service. * @param \Drupal\field\FieldInfo $field_info @@ -78,9 +74,8 @@ public static function createInstance(ContainerInterface $container, $entity_typ * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token * The CSRF token manager service. */ - public function __construct($entity_type, EntityManager $entity_manager, FieldInfo $field_info, ModuleHandlerInterface $module_handler, CsrfTokenGenerator $csrf_token) { - parent::__construct($entity_type); - $this->entityManager = $entity_manager; + public function __construct($entity_type, array $entity_info, EntityManager $entity_manager, FieldInfo $field_info, ModuleHandlerInterface $module_handler, CsrfTokenGenerator $csrf_token) { + parent::__construct($entity_type, $entity_info, $entity_manager); $this->fieldInfo = $field_info; $this->moduleHandler = $module_handler; $this->csrfToken = $csrf_token; diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index b31b8d8..2af6130 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -323,7 +323,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); } /** @@ -627,7 +632,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); @@ -650,28 +658,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..5567afd 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -120,19 +120,21 @@ 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); - } - else { - $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; + } } } } @@ -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 b85380e..0f40e37 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; @@ -858,3 +859,80 @@ 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); } + +/** + * 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 = \Drupal::entityManager()->getTranslationFromContext($entity, $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 0d4d78e..953a81b 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 @@ -15,6 +15,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; @@ -94,6 +95,13 @@ class Field extends FieldPluginBase { protected $languageManager; /** + * The language fallback mapper. + * + * @var \Drupal\Core\Language\LanguageFallbackMapperInterface; + */ + protected $languageFallbackMapper; + + /** * Constructs a \Drupal\field\Plugin\views\field\Field object. * * @param array $configuration @@ -108,13 +116,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; } /** @@ -127,7 +138,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') ); } @@ -261,14 +273,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)); } } @@ -867,11 +872,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 = $this->entityManager->getTranslationFromContext($entity, $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..90d3ef7 100644 --- a/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php @@ -109,6 +109,7 @@ function testFieldFormTranslationRevisions() { $field_name = $this->field->getFieldName(); // Store the field translations. + ksort($available_langcodes); $entity->langcode->value = key($available_langcodes); foreach ($available_langcodes as $langcode => $value) { $entity->getTranslation($langcode)->{$field_name}->value = $value + 1; 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..efe92db 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) { + $candidates = array_reverse($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') { + $candidates = array_reverse($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..ae01612 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/LanguageFallbackMapper.php @@ -0,0 +1,90 @@ +languageManager = $language_manager; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function getCandidateLangcodes($langcode = NULL, 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($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..2db83b5 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/LanguageServiceProvider.php @@ -0,0 +1,30 @@ +getDefinition('language_fallback_mapper') + ->setClass('Drupal\language\LanguageFallbackMapper') + ->addArgument(new Reference('language_manager')) + ->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..083ef41 --- /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(NULL, 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 8e56f58..dfb3ea4 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($this->entityManager()->getTranslationFromContext($node)->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..244a52b 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 = \Drupal::entityManager()->getTranslationFromContext($node, $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..bc0e195 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 = $this->entityManager->getTranslationFromContext($entity, $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 = $this->entityManager->getTranslationFromContext($entity, $langcode2); + $this->assertEqual($translation->language()->id, $langcode, 'The current translation language matches the expected one.'); + + // Check that the current translation is properly returned. + $translation = $this->entityManager->getTranslationFromContext($entity); + $this->assertEqual($langcode, $translation->language()->id, 'The current translation language matches the topmost language fallback candidate.'); + $entity->addTranslation($current_langcode, $values[$current_langcode]); + $translation = $this->entityManager->getTranslationFromContext($entity); + $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 = $this->entityManager->getTranslationFromContext($entity2, $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->getViewBuilder($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 531d6d6..f68cff2 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 @@ -99,8 +99,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..d734ee2 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", + * "view_builder" = "Drupal\entity_test\EntityTestViewBuilder", * "access" = "Drupal\entity_test\EntityTestAccessController", * "form" = { * "default" = "Drupal\entity_test\EntityTestFormController"