diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc index 6d02b9d..7bd994c 100644 --- a/core/modules/content_translation/content_translation.admin.inc +++ b/core/modules/content_translation/content_translation.admin.inc @@ -87,6 +87,8 @@ function _content_translation_form_language_content_settings_form_alter(array &$ // entity might have fields and if there are fields to translate. if (!empty($entity_info['fieldable'])) { $fields = $entity_manager->getFieldDefinitions($entity_type, $bundle); + $translation_metadata = content_translation_entity_field_info($entity_type); + if ($fields) { $form['settings'][$entity_type][$bundle]['translatable'] = array( '#type' => 'checkbox', @@ -121,8 +123,9 @@ function _content_translation_form_language_content_settings_form_alter(array &$ // Instead we need to rely on field definitions to determine whether // fields support translation. Whether they are actually enabled is // determined through our settings. As a consequence only fields - // that support translation can be enabled or disabled. - elseif (isset($field_settings[$field_name]) || !empty($definition['translatable'])) { + // that support translation can be enabled or disabled. We skip + // our own fields as they are always translatable. + elseif (isset($field_settings[$field_name]) || (!empty($definition['translatable']) && !isset($translation_metadata['definitions'][$field_name]))) { $form['settings'][$entity_type][$bundle]['fields'][$field_name] = array( '#label' => $definition['label'], '#type' => 'checkbox', diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 43127b0..323edb5 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -142,6 +142,69 @@ function content_translation_entity_field_info_alter(&$info, $entity_type) { } /** + * Implements hook_entity_field_info(). + */ +function content_translation_entity_field_info($entity_type) { + if (content_translation_enabled($entity_type)) { + $info = array(); + + $info['definitions']['translation_source'] = array( + 'label' => t('Translation source'), + 'description' => t('The source language from which this translation was created.'), + 'type' => 'field_item:language', + 'translatable' => TRUE, + ); + + $info['definitions']['translation_outdated'] = array( + 'label' => t('Translation outdated'), + 'description' => t('A boolean indicating whether this translation needs to be updated.'), + 'type' => 'field_item:boolean', + 'translatable' => TRUE, + ); + + $info['definitions']['translation_uid'] = array( + 'label' => t('Translation author id'), + 'description' => t('The author of this translation.'), + 'type' => 'field_item:entity_reference', + 'settings' => array( + 'target_type' => 'user', + 'default_value' => 0, + ), + 'translatable' => TRUE, + ); + + $info['definitions']['translation_status'] = array( + 'label' => t('Translation status'), + 'description' => t('Boolean indicating whether the translation is visible to non-translators.'), + 'type' => 'field_item:boolean', + 'settings' => array( + 'default_value' => 1, + ), + 'translatable' => TRUE, + ); + + $info['definitions']['translation_created'] = array( + 'label' => t('Translation created time'), + 'description' => t('The Unix timestamp when the translation was created.'), + 'type' => 'field_item:integer', + 'translatable' => TRUE, + ); + + $info['definitions']['translation_changed'] = array( + 'label' => t('Translation changed time'), + 'description' => t('The Unix timestamp when the translation was most recently saved.'), + 'type' => 'field_item:integer', + 'property_constraints' => array( + 'value' => array('EntityChanged' => array()), + ), + 'translatable' => TRUE, + ); + + return $info; + } +} + +/** * Implements hook_menu(). * * @todo Split this into route definition and menu link definition. See @@ -339,7 +402,11 @@ function content_translation_view_access(EntityInterface $entity, $langcode, Acc 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); + if (!isset($account)) { + $account = \Drupal::currentUser(); + } + $status = $entity instanceof ContentEntityInterface ? !empty($entity->getTranslation($langcode)->translation_status->value) : FALSE; + return $status || $account->hasPermission('translate any entity') || $account->hasPermission($permission); } /** @@ -523,7 +590,7 @@ function content_translation_enabled($entity_type, $bundle = NULL) { function content_translation_controller($entity_type) { $entity_info = entity_get_info($entity_type); // @todo Throw an exception if the key is missing. - return new $entity_info['controllers']['translation']($entity_type, $entity_info); + return new $entity_info['controllers']['translation']($entity_type, $entity_info, \Drupal::currentUser()); } /** @@ -692,16 +759,26 @@ function content_translation_load_translation_metadata(array $entities, $entity_ $query = 'SELECT * FROM {content_translation} te WHERE te.entity_type = :entity_type AND te.entity_id IN (:entity_id)'; $result = db_query($query, array(':entity_type' => $entity_type, ':entity_id' => array_keys($entities))); $exclude = array('entity_type', 'entity_id', 'langcode'); + foreach ($result as $record) { $entity = $entities[$record->entity_id]; - // @todo Declare these as entity (translation?) properties. - foreach ($record as $field_name => $value) { - if (!in_array($field_name, $exclude)) { + + foreach ($record as $key => $value) { + if (!in_array($key, $exclude)) { $langcode = $record->langcode; - $entity->translation[$langcode][$field_name] = $value; if (!$entity->hasTranslation($langcode)) { $entity->initTranslation($langcode); } + + $name = 'translation_' . $key; + $item = $entity->getTranslation($langcode)->get($name); + + if ($key != 'uid') { + $item->value = $value; + } + else { + $item->target_id = $value; + } } } } @@ -716,14 +793,26 @@ function content_translation_entity_insert(EntityInterface $entity) { return; } + $definitions = $entity->getPropertyDefinitions(); $fields = array('entity_type', 'entity_id', 'langcode', 'source', 'outdated', 'uid', 'status', 'created', 'changed'); $query = db_insert('content_translation')->fields($fields); foreach ($entity->getTranslationLanguages() as $langcode => $language) { - $translation = isset($entity->translation[$langcode]) ? $entity->translation[$langcode] : array(); + $translation = $entity->getTranslation($langcode); + + $record = array(); + foreach ($fields as $key) { + $name = 'translation_' . $key; + if (isset($definitions[$name])) { + $item = $translation->get($name); + if (isset($item->value)) { + $record[$key] = $item->value; + } + } + } - $translation += array( - 'source' => '', + $record += array( + 'source' => Language::LANGCODE_NOT_SPECIFIED, 'uid' => $GLOBALS['user']->id(), 'outdated' => FALSE, 'status' => TRUE, @@ -731,15 +820,14 @@ function content_translation_entity_insert(EntityInterface $entity) { 'changed' => REQUEST_TIME, ); - $translation['entity_type'] = $entity->entityType(); - $translation['entity_id'] = $entity->id(); - $translation['langcode'] = $langcode; + $record['entity_type'] = $entity->entityType(); + $record['entity_id'] = $entity->id(); + $record['langcode'] = $langcode; // Reorder values to match the schema. $values = array(); - foreach ($fields as $field_name) { - $value = is_bool($translation[$field_name]) ? intval($translation[$field_name]) : $translation[$field_name]; - $values[$field_name] = $value; + foreach ($fields as $key) { + $values[$key] = is_bool($record[$key]) ? intval($record[$key]) : $record[$key]; } $query->values($values); } @@ -862,11 +950,16 @@ function content_translation_field_info_alter(&$info) { * Implements hook_entity_presave(). */ function content_translation_entity_presave(EntityInterface $entity) { - if ($entity instanceof ContentEntityInterface && $entity->isTranslatable()) { - // @todo Avoid using request attributes once translation metadata become - // regular fields. - $attributes = \Drupal::request()->attributes; - \Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $entity->language()->id, $attributes->get('source_langcode')); + if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && !$entity->isNew()) { + // If we are creating a new translation we need to use the source language + // as original language, since source values are the only ones available to + // compare against. + if (!isset($entity->original)) { + $entity->original = entity_load_unchanged($entity->entityType(), $entity->id()); + } + $langcode = $entity->language()->id; + $source_langcode = !$entity->original->hasTranslation($langcode) ? $entity->translation_source->id : NULL; + \Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $langcode, $source_langcode); } } diff --git a/core/modules/content_translation/content_translation.pages.inc b/core/modules/content_translation/content_translation.pages.inc index b6473ae..9797964 100644 --- a/core/modules/content_translation/content_translation.pages.inc +++ b/core/modules/content_translation/content_translation.pages.inc @@ -71,7 +71,8 @@ function content_translation_overview(EntityInterface $entity) { if (isset($translations[$langcode])) { // Existing translation in the translation set: display status. - $source = isset($entity->translation[$langcode]['source']) ? $entity->translation[$langcode]['source'] : ''; + $translation = $entity->getTranslation($langcode); + $source = isset($translation->translation_source) ? $translation->translation_source->value : Language::LANGCODE_NOT_SPECIFIED; $is_original = $langcode == $original; $label = $entity->getTranslation($langcode)->label(); $link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $rel['canonical']['path'], 'language' => $language); @@ -95,10 +96,9 @@ function content_translation_overview(EntityInterface $entity) { $links['edit']['title'] = t('Edit'); } - $translation = $entity->translation[$langcode]; - $status = !empty($translation['status']) ? t('Published') : t('Not published'); + $status = !empty($translation->translation_status->value) ? t('Published') : t('Not published'); // @todo Add a theming function here. - $status = '' . $status . '' . (!empty($translation['outdated']) ? ' ' . t('outdated') . '' : ''); + $status = '' . $status . '' . (!empty($translation->translation_outdated->value) ? ' ' . t('outdated') . '' : ''); if ($is_original) { $language_name = t('@language_name (Original language)', array('@language_name' => $language_name)); diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationManageAccessCheck.php b/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationManageAccessCheck.php index 16cc31a..ea5e3b0 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationManageAccessCheck.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/Access/ContentTranslationManageAccessCheck.php @@ -52,7 +52,7 @@ public function access(Route $route, Request $request, AccountInterface $account $route_requirements = $route->getRequirements(); $operation = $route_requirements['_access_content_translation_manage']; $controller_class = $this->entityManager->getControllerClass($entity_type, 'translation'); - $controller = new $controller_class($entity_type, $entity->entityInfo()); + $controller = new $controller_class($entity_type, $entity->entityInfo(), $account); // Load translation. $translations = $entity->getTranslationLanguages(); diff --git a/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationController.php b/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationController.php index 634043b..a34efd7 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationController.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/ContentTranslationController.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Session\AccountInterface; /** * Base class for content translation controllers. @@ -30,16 +31,26 @@ class ContentTranslationController implements ContentTranslationControllerInterf protected $entityInfo; /** + * The user who is translating the entity. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** * Initializes an instance of the content translation controller. * * @param string $entity_type * The type of the entity being translated. * @param array $entity_info * The info array of the given entity type. + * @param \Drupal\Core\Session\AccountInterface + * The user who is translating the entity. */ - public function __construct($entity_type, $entity_info) { + public function __construct($entity_type, $entity_info, $current_user) { $this->entityType = $entity_type; $this->entityInfo = $entity_info; + $this->currentUser = $current_user; } /** @@ -49,7 +60,7 @@ public function retranslate(EntityInterface $entity, $langcode = NULL) { $updated_langcode = !empty($langcode) ? $langcode : $entity->language()->id; $translations = $entity->getTranslationLanguages(); foreach ($translations as $langcode => $language) { - $entity->translation[$langcode]['outdated'] = $langcode != $updated_langcode; + $entity->getTranslation($langcode)->translation_outdated->value = $langcode != $updated_langcode; } } @@ -191,7 +202,7 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac ); // A new translation is enabled by default. - $status = $new_translation || $entity->translation[$form_langcode]['status']; + $status = $new_translation || $entity->translation_status->value; // If there is only one published translation we cannot unpublish it, // since there would be nothing left to display. $enabled = TRUE; @@ -199,8 +210,8 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac // A new translation is not available in the translation metadata, hence // it should count as one more. $published = $new_translation; - foreach ($entity->translation as $translation) { - $published += $translation['status']; + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + $published += $entity->getTranslation($langcode)->translation_status->value; } $enabled = $published > 1; } @@ -216,7 +227,7 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac '#disabled' => !$enabled, ); - $translate = !$new_translation && $entity->translation[$form_langcode]['outdated']; + $translate = !$new_translation && $entity->translation_outdated->value; if (!$translate) { $form['content_translation']['retranslate'] = array( '#type' => 'checkbox', @@ -237,10 +248,10 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac // Default to the anonymous user. $name = ''; if ($new_translation) { - $name = $GLOBALS['user']->getUsername(); + $name = $this->currentUser->getUsername(); } - elseif ($entity->translation[$form_langcode]['uid']) { - $name = user_load($entity->translation[$form_langcode]['uid'])->getUsername(); + elseif ($entity->translation_uid->value) { + $name = $entity->translation_uid->entity->getUsername(); } $form['content_translation']['name'] = array( '#type' => 'textfield', @@ -251,7 +262,7 @@ public function entityFormAlter(array &$form, array &$form_state, EntityInterfac '#description' => t('Leave blank for %anonymous.', array('%anonymous' => \Drupal::config('user.settings')->get('anonymous'))), ); - $date = $new_translation ? REQUEST_TIME : $entity->translation[$form_langcode]['created']; + $date = $new_translation ? REQUEST_TIME : $entity->translation_created->value; $form['content_translation']['created'] = array( '#type' => 'textfield', '#title' => t('Authored on'), @@ -373,35 +384,22 @@ protected function addTranslatabilityClue(&$element) { public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, array &$form_state) { $form_controller = content_translation_form_controller($form_state); $form_langcode = $form_controller->getFormLangcode($form_state); - - if (!isset($entity->translation[$form_langcode])) { - $entity->translation[$form_langcode] = array(); - } $values = isset($form_state['values']['content_translation']) ? $form_state['values']['content_translation'] : array(); - $translation = &$entity->translation[$form_langcode]; - // @todo Use the entity setter when all entities support multilingual - // properties. - $translation['uid'] = !empty($values['name']) && ($account = user_load_by_name($values['name'])) ? $account->id() : 0; - $translation['status'] = !empty($values['status']); - $translation['created'] = !empty($values['created']) ? strtotime($values['created']) : REQUEST_TIME; - $translation['changed'] = REQUEST_TIME; + $entity->translation_uid->target_id = !empty($values['name']) && ($account = user_load_by_name($values['name'])) ? $account->id() : 0; + $entity->translation_status->value = !empty($values['status']); + $entity->translation_created->value = !empty($values['created']) ? strtotime($values['created']) : REQUEST_TIME; + $entity->translation_changed->value = REQUEST_TIME; $source_langcode = $this->getSourceLangcode($form_state); if ($source_langcode) { - $translation['source'] = $source_langcode; + $entity->translation_source->value = $source_langcode; } - $translation['outdated'] = !empty($values['outdated']); + $entity->translation_outdated->value = !empty($values['outdated']); if (!empty($values['retranslate'])) { $this->retranslate($entity, $form_langcode); } - - // Set contextual information that can be reused during the storage phase. - // @todo Remove this once translation metadata are converted to regular - // fields. - $attributes = \Drupal::request()->attributes; - $attributes->set('source_langcode', $source_langcode); } /** diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php index f974837..c5e8d9b 100644 --- a/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php +++ b/core/modules/content_translation/lib/Drupal/content_translation/Tests/ContentTranslationSyncImageTest.php @@ -122,10 +122,6 @@ function testImageFieldSync() { $default_langcode = $this->langcodes[0]; $langcode = $this->langcodes[1]; - // Populate the required contextual values. - $attributes = $this->container->get('request')->attributes; - $attributes->set('source_langcode', $default_langcode); - // Populate the test entity with some random initial values. $values = array( 'name' => $this->randomName(), @@ -193,6 +189,7 @@ function testImageFieldSync() { // Perform synchronization: the translation language is used as source, // while the default language is used as target. + $translation->translation_source->id = $default_langcode; $entity = $this->saveEntity($translation); $translation = $entity->getTranslation($langcode); @@ -222,8 +219,6 @@ function testImageFieldSync() { 'title' => $langcode . '_' . $removed_fid . '_' . $this->randomName(), ); $translation->{$this->fieldName}->setValue(array_values($values[$langcode])); - // When updating an entity we do not have a source language defined. - $attributes->remove('source_langcode'); $entity = $this->saveEntity($translation); $translation = $entity->getTranslation($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 8a1b198..4b27cae 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 @@ -146,7 +146,7 @@ protected function doTestOutdatedStatus() { $this->drupalGet($path); $this->assertFieldByXPath('//input[@name="content_translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.'); $entity = entity_load($this->entityType, $this->entityId, TRUE); - $this->assertFalse($entity->translation[$enabled_langcode]['outdated'], 'The "outdated" status has been correctly stored.'); + $this->assertFalse($entity->getTranslation($enabled_langcode)->translation_outdated->value, 'The "outdated" status has been correctly stored.'); } } } @@ -165,7 +165,7 @@ protected function doTestPublishedStatus() { $edit = array('content_translation[status]' => FALSE); $this->drupalPostForm($langcode . '/' . $path, $edit, $this->getFormSubmitAction($entity)); $entity = entity_load($this->entityType, $this->entityId, TRUE); - $this->assertFalse($entity->translation[$langcode]['status'], 'The translation has been correctly unpublished.'); + $this->assertFalse($entity->getTranslation($langcode)->translation_status->value, 'The translation has been correctly unpublished.'); } } @@ -200,8 +200,9 @@ protected function doTestAuthoringInfo() { $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.'); + $translation = $entity->getTranslation($langcode); + $this->assertEqual($translation->translation_uid->target_id == $values[$langcode]['uid'], 'Translation author correctly stored.'); + $this->assertEqual($translation->translation_created->value == $values[$langcode]['created'], 'Translation date correctly stored.'); } // Try to post non valid values and check that they are rejected. @@ -213,8 +214,9 @@ protected function doTestAuthoringInfo() { ); $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.'); + $translation = $entity->getTranslation($langcode); + $this->assertEqual($translation->translation_uid->target_id == $values[$langcode]['uid'], 'Translation author correctly kept.'); + $this->assertEqual($translation->translation_created->value == $values[$langcode]['created'], 'Translation date correctly kept.'); } /** diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php index 25cce23..175cdcf 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php @@ -133,7 +133,7 @@ protected function doTestPublishedStatus() { // The node is created as unpulished thus we switch to the published // status first. $status = !$index; - $this->assertEqual($status, $entity->translation[$langcode]['status'], 'The translation has been correctly unpublished.'); + $this->assertEqual($status, $entity->getTranslation($langcode)->translation_status->value, 'The translation has been correctly unpublished.'); } } } @@ -164,8 +164,9 @@ protected function doTestAuthoringInfo() { $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.'); + $translation = $entity->getTranslation($langcode); + $this->assertEqual($translation->translation_uid->target_id == $values[$langcode]['uid'], 'Translation author correctly stored.'); + $this->assertEqual($translation->translation_created->value == $values[$langcode]['created'], 'Translation date correctly stored.'); } }