diff --git a/core/includes/entity.inc b/core/includes/entity.inc index fd5a8808430b4f329c7ac68b8c56801259f5d65c..8cb009a6357ba9e78f601d0a1260a6e8d0759175 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -53,6 +53,21 @@ function entity_info_cache_clear() { cache()->invalidateTags(array('entity_info' => TRUE)); } +/** + * Returns the defined bundles for the given entity type. + * + * @param string $entity_type + * The entity type whose bundles should be returned. + * + * @return array + * An array containing the bundle names or the entity type name itself if no + * bundle is defined. + */ +function entity_get_bundles($entity_type) { + $entity_info = entity_get_info($entity_type); + return isset($entity_info['bundles']) ? array_keys($entity_info['bundles']) : array($entity_type); +} + /** * Loads an entity from the database. * @@ -445,7 +460,7 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st // Invoke all specified builders for copying form values to entity properties. if (isset($form['#entity_builders'])) { foreach ($form['#entity_builders'] as $function) { - $function($entity_type, $entity, $form, $form_state); + call_user_func_array($function, array($entity_type, $entity, &$form, &$form_state)); } } diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 710792fa017a141a40b74ca7d050285a27573f7a..c6e041ea206fcc7a4bc6a2df347cd06a2c2403c8 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -275,37 +275,43 @@ public function getTranslation($langcode, $strict = TRUE) { /** * Returns the languages the entity is translated to. * - * @todo: Remove once all entity types implement the entity field API. This - * is deprecated by - * TranslatableInterface::getTranslationLanguages(). + * @todo: Remove once all entity types implement the entity field API. + * This is deprecated by + * Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages(). */ public function translations() { - $languages = array(); + return $this->getTranslationLanguages(FALSE); + } + + /** + * Implements TranslatableInterface::getTranslationLanguages(). + */ + public function getTranslationLanguages($include_default = TRUE) { + // @todo: Replace by EntityNG implementation once all entity types have been + // converted to use the entity field API. + $default_language = $this->language(); + $languages = array($default_language->langcode => $default_language); $entity_info = $this->entityInfo(); - if ($entity_info['fieldable'] && ($default_language = $this->language())) { + + if ($entity_info['fieldable']) { // Go through translatable properties and determine all languages for // which translated values are available. foreach (field_info_instances($this->entityType, $this->bundle()) as $field_name => $instance) { $field = field_info_field($field_name); if (field_is_translatable($this->entityType, $field) && isset($this->$field_name)) { - foreach ($this->$field_name as $langcode => $value) { + foreach (array_filter($this->$field_name) as $langcode => $value) { $languages[$langcode] = TRUE; } } } - // Remove the default language from the translations. + $languages = array_intersect_key(language_list(LANGUAGE_ALL), $languages); + } + + if (empty($include_default)) { unset($languages[$default_language->langcode]); - $languages = array_intersect_key(language_list(), $languages); } - return $languages; - } - /** - * Implements TranslatableInterface::getTranslationLanguages(). - */ - public function getTranslationLanguages($include_default = TRUE) { - // @todo: Replace by EntityNG implementation once all entity types have been - // converted to use the entity field API. + return $languages; } /** diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php index f75015f764754d63a44694c574f0f1b021ee286e..2e9daacdec011e3141ee2a25a5f6a1b652470a42 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -178,6 +178,7 @@ public function validate(array $form, array &$form_state) { * A reference to a keyed array containing the current state of the form. */ public function submit(array $form, array &$form_state) { + $this->submitEntityLanguage($form, $form_state); $entity = $this->buildEntity($form, $form_state); $this->setEntity($entity, $form_state); return $entity; @@ -212,7 +213,7 @@ public function delete(array $form, array &$form_state) { */ public function getFormLangcode(array $form_state) { $entity = $this->getEntity($form_state); - $translations = $entity->translations(); + $translations = $entity->getTranslationLanguages(); if (!empty($form_state['langcode'])) { $langcode = $form_state['langcode']; @@ -233,6 +234,54 @@ public function getFormLangcode(array $form_state) { return !empty($langcode) ? $langcode : $entity->language()->langcode; } + /** + * Implements EntityFormControllerInterface::isDefaultFormLangcode(). + */ + public function isDefaultFormLangcode($form_state) { + return $this->getFormLangcode($form_state) == $this->getEntity($form_state)->language()->langcode; + } + + /** + * Handle possible entity language changes. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * A reference to a keyed array containing the current state of the form. + */ + protected function submitEntityLanguage(array $form, array &$form_state) { + // Update the form language as it might have changed. + if (isset($form_state['values']['langcode']) && $this->isDefaultFormLangcode($form_state)) { + $form_state['langcode'] = $form_state['values']['langcode']; + } + + $entity = $this->getEntity($form_state); + $entity_type = $entity->entityType(); + + if (field_has_translation_handler($entity_type)) { + $form_langcode = $this->getFormLangcode($form_state); + + // If we are editing the default language values, we use the submitted + // entity language as the new language for fields to handle any language + // change. Otherwise the current form language is the proper value, since + // in this case it is not supposed to change. + $current_langcode = $entity->language()->langcode == $form_langcode ? $form_state['values']['langcode'] : $form_langcode; + + foreach (field_info_instances($entity_type, $entity->bundle()) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + $previous_langcode = $form[$field_name]['#language']; + + // Handle a possible language change: new language values are inserted, + // previous ones are deleted. + if ($field['translatable'] && $previous_langcode != $current_langcode) { + $form_state['values'][$field_name][$current_langcode] = $form_state['values'][$field_name][$previous_langcode]; + $form_state['values'][$field_name][$previous_langcode] = array(); + } + } + } + } + /** * Implements Drupal\Core\Entity\EntityFormControllerInterface::buildEntity(). */ diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php index 26a227ded3bc401cf3a0cff47ce0d85427f61043..fecb0783ff192f509d9e08bfef8ea027a8656ab2 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php @@ -43,6 +43,17 @@ public function build(array $form, array &$form_state, EntityInterface $entity); */ public function getFormLangcode(array $form_state); + /** + * Checks whether the current form language matches the entity one. + * + * @param array $form_state + * A reference to a keyed array containing the current state of the form. + * + * @return boolean + * Returns TRUE if the entity form language matches the entity one. + */ + public function isDefaultFormLangcode($form_state); + /** * Returns the operation identifying the form controller. * diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php index d06614ddd4bd6baf405c2b354c5ef6e0243e85e4..812a192bdef2a5000212dea028f2ad4d0487edaa 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php @@ -64,14 +64,19 @@ public function buildEntity(array $form, array &$form_state) { // without changing existing entity properties that are not being edited by // this form. Copying field values must be done using field_attach_submit(). $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $entity->bundle())) : $form_state['values']; + $translation = $entity->getTranslation($this->getFormLangcode($form_state), FALSE); + $definitions = $translation->getPropertyDefinitions(); foreach ($values_excluding_fields as $key => $value) { - $entity->$key = $value; + if (isset($definitions[$key])) { + $translation->$key = $value; + } } - // Invoke all specified builders for copying form values to entity properties. + // Invoke all specified builders for copying form values to entity + // properties. if (isset($form['#entity_builders'])) { foreach ($form['#entity_builders'] as $function) { - $function($entity_type, $entity, $form, $form_state); + call_user_func_array($function, array($entity_type, $entity, &$form, &$form_state)); } } diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index b8c38ce773db3c614c0154bb8cc50d383c12454f..29f0a36dcc4dd8378f0be767f7cbab8788877219 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -59,6 +59,10 @@ * Drupal\Core\Entity\EntityListController. * - render_controller_class: The name of the class that is used to render the * entities. Defaults to Drupal\Core\Entity\EntityRenderController. + * - translation_controller_class: (optional) The name of the translation + * controller class that should be used to handle the translation process. + * See Drupal\translation_entity\EntityTranslationControllerInterface for more + * information. * - static_cache: (optional) Boolean indicating whether entities should be * statically cached during a page request. Used by * Drupal\Core\Entity\DatabaseStorageController. Defaults to TRUE. @@ -140,6 +144,16 @@ * by default (e.g. right after the module exposing the view mode is * enabled), but administrators can later use the Field UI to apply custom * display settings specific to the view mode. + * - menu_base_path: (optional) The base menu router path to which the entity + * administration user interface responds. It can be used to generate UI + * links and to attach additional router items to the entity UI in a generic + * fashion. + * - menu_view_path: (optional) The menu router path to be used to view the + * entity. + * - menu_edit_path: (optional) The menu router path to be used to edit the + * entity. + * - menu_path_wildcard: (optional) A string identifying the menu loader in the + * router path. * * The defaults for the plugin definition are provided in * \Drupal\Core\Entity\EntityManager::defaults. diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 2fc77644ee33821fbadde66dcb6f5f8c1008b0e7..35ef89d20e1e33b05b63731bf6290eada3a24c15 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -269,20 +269,12 @@ public function getTranslationLanguages($include_default = TRUE) { $translations[$this->language()->langcode] = TRUE; } - // Now get languages based upon translation langcodes. - $languages = array_intersect_key(language_list(LANGUAGE_ALL), $translations); + // Now get languages based upon translation langcodes. Empty languages must + // be filtered out as they concern empty/unset properties. + $languages = array_intersect_key(language_list(LANGUAGE_ALL), array_filter($translations)); return $languages; } - /** - * Overrides Entity::translations(). - * - * @todo: Remove once Entity::translations() gets removed. - */ - public function translations() { - return $this->getTranslationLanguages(FALSE); - } - /** * Enables or disable the compatibility mode. * diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index c174293bba4a61c977be1b36a28c06f69343eb7e..7b6eda44ab427dc06e0962e829403b93844342f5 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -1009,6 +1009,16 @@ function comment_links(Comment $comment, Node $node) { $links['comment-forbidden']['html'] = TRUE; } } + + // Add translations link for translation-enabled comment bundles. + if (module_exists('translation_entity') && translation_entity_translate_access($comment)) { + $links['comment-translations'] = array( + 'title' => t('translations'), + 'href' => 'comment/' . $comment->id() . '/translations', + 'html' => TRUE, + ); + } + return $links; } @@ -1106,9 +1116,38 @@ function comment_form_node_type_form_alter(&$form, $form_state) { DRUPAL_REQUIRED => t('Required'), ), ); + // @todo Remove this check once language settings are generalized. + if (module_exists('translation_entity')) { + $comment_form = $form; + $comment_form_state['translation_entity']['key'] = 'language_configuration'; + $form['comment'] += translation_entity_enable_widget('comment', 'comment_node_' . $form['#node_type']->type, $comment_form, $comment_form_state); + array_unshift($form['#submit'], 'comment_translation_configuration_element_submit'); + } } } +/** + * Form submission handler for node_type_form(). + * + * This handles the comment translation settings added by + * comment_form_node_type_form_alter(). + * + * @see comment_form_node_type_form_alter() + */ +function comment_translation_configuration_element_submit($form, &$form_state) { + // The comment translation settings form element is embedded into the node + // type form. Hence we need to provide to the regular submit handler a + // manipulated form state to make it process comment settings instead of node + // settings. + $key = 'language_configuration'; + $comment_form_state = array( + 'translation_entity' => array('key' => $key), + 'language' => array($key => array('entity_type' => 'comment', 'bundle' => 'comment_node_' . $form['#node_type']->type)), + 'values' => array($key => array('translation_entity' => $form_state['values']['translation_entity'])), + ); + translation_entity_language_configuration_element_submit($form, $comment_form_state); +} + /** * Implements hook_form_BASE_FORM_ID_alter(). */ diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php index cead0cd17deb3298fe4de68193cb1ee36785d903..45ae3df9b8303a6745562dabc284312070c86f05 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentFormController.php +++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php @@ -220,8 +220,6 @@ protected function actions(array $form, array &$form_state) { ), ); - $element['#weight'] = $form['comment_body']['#weight'] + 0.01; - return $element; } diff --git a/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php b/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php new file mode 100644 index 0000000000000000000000000000000000000000..0cfa52b20899b0e8a3ec2203fbca6d8e70a3bab8 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php @@ -0,0 +1,26 @@ + $entity->label())); + } + +} diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php index 2b00d4557f9d30c766a7bf2807e01d093cd469a7..e0bbfd9ae4bc12215fefbc95a6229ffc42ab9a57 100644 --- a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php +++ b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php @@ -24,6 +24,7 @@ * form_controller_class = { * "default" = "Drupal\comment\CommentFormController" * }, + * translation_controller_class = "Drupal\comment\CommentTranslationController", * base_table = "comment", * uri_callback = "comment_uri", * fieldable = TRUE, diff --git a/core/modules/comment/lib/Drupal/comment/Tests/CommentTranslationUITest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentTranslationUITest.php new file mode 100644 index 0000000000000000000000000000000000000000..75ca1d8a9936e5622a545c7db072e875ba85ba6c --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentTranslationUITest.php @@ -0,0 +1,95 @@ + 'Comment translation UI', + 'description' => 'Tests the basic comment translation UI.', + 'group' => 'Comment', + ); + } + + /** + * Overrides \Drupal\simpletest\WebTestBase::setUp(). + */ + function setUp() { + $this->entityType = 'comment'; + $this->nodeBundle = 'article'; + $this->bundle = 'comment_node_' . $this->nodeBundle; + $this->testLanguageSelector = FALSE; + $this->subject = $this->randomName(); + parent::setUp(); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::setupBundle(). + */ + function setupBundle() { + parent::setupBundle(); + $this->drupalCreateContentType(array('type' => $this->nodeBundle, 'name' => $this->nodeBundle)); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission(). + */ + function getTranslatorPermissions() { + return array('post comments', 'administer comments', "translate $this->entityType entities", 'edit original values'); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::setupTestFields(). + */ + function setupTestFields() { + parent::setupTestFields(); + $field = field_info_field('comment_body'); + $field['translatable'] = TRUE; + field_update_field($field); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::createEntity(). + */ + protected function createEntity($values, $langcode) { + $node = $this->drupalCreateNode(array('type' => $this->nodeBundle)); + $values['nid'] = $node->nid; + $values['uid'] = $node->uid; + return parent::createEntity($values, $langcode); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues(). + */ + protected function getNewEntityValues($langcode) { + // Comment subject is not translatable hence we use a fixed value. + return array( + 'subject' => $this->subject, + 'comment_body' => array(array('value' => $this->randomString(16))), + ) + parent::getNewEntityValues($langcode); + } + +} diff --git a/core/modules/node/content_types.inc b/core/modules/node/content_types.inc index a7faa5994739a33a7e20673aa2ab8568292b2252..a6839ae07b3ccc6575420eac2d7c72318e62d63f 100644 --- a/core/modules/node/content_types.inc +++ b/core/modules/node/content_types.inc @@ -224,6 +224,7 @@ function node_type_form($form, &$form_state, $type = NULL) { ), '#default_value' => $language_configuration, ); + $form['#submit'][] = 'language_configuration_element_submit'; } $form['display'] = array( diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php index 5add3df9d9f0c4caa8aeceb2ddc0bd55c0e504cd..563a00fc78895fe4fdcabfef517ef042a0b8fee9 100644 --- a/core/modules/node/lib/Drupal/node/NodeFormController.php +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -317,8 +317,6 @@ public function validate(array $form, array &$form_state) { * Overrides Drupal\Core\Entity\EntityFormController::submit(). */ public function submit(array $form, array &$form_state) { - $this->submitNodeLanguage($form, $form_state); - // Build the node object from the submitted values. $node = parent::submit($form, $form_state); @@ -336,36 +334,6 @@ public function submit(array $form, array &$form_state) { return $node; } - /** - * Handle possible node language changes. - */ - protected function submitNodeLanguage(array $form, array &$form_state) { - if (field_has_translation_handler('node', 'node')) { - $bundle = $form_state['values']['type']; - $entity = $this->getEntity($form_state); - $form_langcode = $this->getFormLangcode($form_state); - - // If we are editing the default language values, we use the submitted - // entity language as the new language for fields to handle any language - // change. Otherwise the current form language is the proper value, since - // in this case it is not supposed to change. - $current_langcode = $entity->language()->langcode == $form_langcode ? $form_state['values']['langcode'] : $form_langcode; - - foreach (field_info_instances('node', $bundle) as $instance) { - $field_name = $instance['field_name']; - $field = field_info_field($field_name); - $previous_langcode = $form[$field_name]['#language']; - - // Handle a possible language change: new language values are inserted, - // previous ones are deleted. - if ($field['translatable'] && $previous_langcode != $current_langcode) { - $form_state['values'][$field_name][$current_langcode] = $form_state['values'][$field_name][$previous_langcode]; - $form_state['values'][$field_name][$previous_langcode] = array(); - } - } - } - } - /** * Form submission handler for the 'preview' action. * diff --git a/core/modules/node/lib/Drupal/node/NodeTranslationController.php b/core/modules/node/lib/Drupal/node/NodeTranslationController.php new file mode 100644 index 0000000000000000000000000000000000000000..da078ac64f50182f1891c20449d70ffb294358b5 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/NodeTranslationController.php @@ -0,0 +1,50 @@ + 'additional_settings', + '#weight' => 100, + '#attributes' => array( + 'class' => array('node-translation-options'), + ), + ); + } + } + + /** + * Overrides EntityTranslationController::entityFormTitle(). + */ + protected function entityFormTitle(EntityInterface $entity) { + $type_name = node_get_type_label($entity); + return t('Edit @type @title', array('@type' => $type_name, '@title' => $entity->label())); + } +} diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php index f161d81d34f92f4ec2e65f71efae35d6b8b0db1c..a9139e9de4f32ae40dd7d03d0447e5724e3658ff 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php +++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php @@ -24,6 +24,7 @@ * form_controller_class = { * "default" = "Drupal\node\NodeFormController" * }, + * translation_controller_class = "Drupal\node\NodeTranslationController", * base_table = "node", * revision_table = "node_revision", * uri_callback = "node_uri", diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php new file mode 100644 index 0000000000000000000000000000000000000000..17c7fecff34ab97f12e5ae8b3b45b0e9730d6b50 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php @@ -0,0 +1,70 @@ + 'Node translation UI', + 'description' => 'Tests the node translation UI.', + 'group' => 'Node', + ); + } + + /** + * Overrides \Drupal\simpletest\WebTestBase::setUp(). + */ + function setUp() { + $this->entityType = 'node'; + $this->bundle = 'article'; + $this->title = $this->randomName(); + parent::setUp(); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::setupBundle(). + */ + protected function setupBundle() { + parent::setupBundle(); + $this->drupalCreateContentType(array('type' => $this->bundle, 'name' => $this->bundle)); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission(). + */ + function getTranslatorPermissions() { + return array("edit any $this->bundle content", "translate $this->entityType entities", 'edit original values'); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues(). + */ + protected function getNewEntityValues($langcode) { + // Node title is not translatable yet, hence we use a fixed value. + return array('title' => $this->title) + parent::getNewEntityValues($langcode); + } + +} diff --git a/core/modules/node/node.js b/core/modules/node/node.js index 0899d3c50cb09e995c8809e172f7a84922bf3db6..e54a4d46de0040cbb82cdb3153cb0514d45bbb9c 100644 --- a/core/modules/node/node.js +++ b/core/modules/node/node.js @@ -42,6 +42,21 @@ Drupal.behaviors.nodeFieldsetSummaries = { } return vals.join(', '); }); + + $context.find('fieldset.node-translation-options').drupalSetSummary(function (context) { + var translate; + var $checkbox = $context.find('.form-item-translation-translate input'); + + if ($checkbox.size()) { + translate = $checkbox.is(':checked') ? Drupal.t('Needs to be updated') : Drupal.t('Does not need to be updated'); + } + else { + $checkbox = $context.find('.form-item-translation-retranslate input'); + translate = $checkbox.is(':checked') ? Drupal.t('Flag other translations as outdated') : Drupal.t('Do not flag other translations as outdated'); + } + + return translate; + }); } }; diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 5c1142423c9a965ccea5a0d17c5554fddbec5736..fc53461c7df05a359e1238d247325c81d67fb380 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -264,6 +264,8 @@ function node_admin_paths() { 'node/*/revisions' => TRUE, 'node/*/revisions/*/revert' => TRUE, 'node/*/revisions/*/delete' => TRUE, + 'node/*/translations' => TRUE, + 'node/*/translations/*' => TRUE, 'node/add' => TRUE, 'node/add/*' => TRUE, ); @@ -2449,7 +2451,7 @@ function node_update_index() { $counter = 0; foreach (node_load_multiple($nids) as $node) { // Determine when the maximum number of indexable items is reached. - $counter += 1 + count($node->translations()); + $counter += count($node->getTranslationLanguages()); if ($counter > $limit) { break; } @@ -2469,7 +2471,7 @@ function _node_index_node(Node $node) { // results half-life calculation. variable_set('node_cron_last', $node->changed); - $languages = array_merge(array(language_load($node->langcode)), $node->translations()); + $languages = $node->getTranslationLanguages(); foreach ($languages as $language) { // Render the node. diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index 9ee32ec97a06a265a08f3588cd76a171df63a626..7844dcbdeb2165df8643de701ad034dcaceb7e6e 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -5,6 +5,9 @@ * Test module for the entity API providing an entity type for testing. */ +use Drupal\entity_test\Plugin\Core\Entity\EntityTest; + + /** * Implements hook_entity_info_alter(). */ @@ -59,6 +62,11 @@ function entity_test_menu() { /** * Menu callback: displays the 'Add new entity_test' form. + * + * @return array + * The processed form for a new entity_test. + * + * @see entity_test_menu() */ function entity_test_add() { drupal_set_title(t('Create an entity_test')); @@ -68,9 +76,17 @@ function entity_test_add() { /** * Menu callback: displays the 'Edit existing entity_test' form. + * + * @param array $entity + * The entity to be edited. + * + * @return array + * The processed form for the edited entity_test. + * + * @see entity_test_menu() */ -function entity_test_edit($entity) { - drupal_set_title(t('entity_test @id', array('@id' => $entity->id())), PASS_THROUGH); +function entity_test_edit(EntityTest $entity) { + drupal_set_title($entity->label(), PASS_THROUGH); return entity_get_form($entity); } diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php index e51f4742bc9b151e7e30d0961e1f39f5e3a0d372..a65f06fb78d4f2d2ade305ccf6ed4a13a4c008f5 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php @@ -43,21 +43,14 @@ public function form(array $form, array &$form_state, EntityInterface $entity) { '#weight' => -10, ); - return $form; - } + $form['langcode'] = array( + '#title' => t('Language'), + '#type' => 'language_select', + '#default_value' => $entity->language()->langcode, + '#languages' => LANGUAGE_ALL, + ); - /** - * Overrides Drupal\Core\Entity\EntityFormController::submit(). - */ - public function submit(array $form, array &$form_state) { - $entity = parent::submit($form, $form_state); - $langcode = $this->getFormLangcode($form_state); - // Updates multilingual properties. - $translation = $entity->getTranslation($langcode); - foreach (array('name', 'user_id') as $name) { - $translation->$name->setValue($form_state['values'][$name]); - } - return $entity; + return $form; } /** diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php index fbb735da679392518e0211bca19a9cc27187a560..da502b88fc3d561c5b832393cf7dba31d2427c8f 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php @@ -84,6 +84,13 @@ protected function attachPropertyData(&$queried_entities) { protected function postSave(EntityInterface $entity, $update) { $default_langcode = $entity->language()->langcode; + // Delete and insert to handle removed values. + db_delete('entity_test_property_data') + ->condition('id', $entity->id()) + ->execute(); + + $query = db_insert('entity_test_property_data'); + foreach ($entity->getTranslationLanguages() as $langcode => $language) { $translation = $entity->getTranslation($langcode); @@ -95,12 +102,12 @@ protected function postSave(EntityInterface $entity, $update) { 'user_id' => $translation->user_id->value, ); - db_merge('entity_test_property_data') - ->fields($values) - ->condition('id', $values['id']) - ->condition('langcode', $values['langcode']) - ->execute(); + $query + ->fields(array_keys($values)) + ->values($values); } + + $query->execute(); } /** diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php new file mode 100644 index 0000000000000000000000000000000000000000..e7ca0507b835a285e458ac37929e3413ce1de9e6 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php @@ -0,0 +1,28 @@ +getTranslation($langcode); + foreach ($translation->getPropertyDefinitions() as $property_name => $langcode) { + $translation->$property_name = array(); + } + } + +} diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php index 240c5c7dee9c8773821e10429b0709257f460540..3c948bd062a359d9dd5576c05ce0e608f4b25d97 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php @@ -22,13 +22,15 @@ * form_controller_class = { * "default" = "Drupal\entity_test\EntityTestFormController" * }, + * translation_controller_class = "Drupal\entity_test\EntityTestTranslationController", * base_table = "entity_test", * data_table = "entity_test_property_data", * fieldable = TRUE, * entity_keys = { * "id" = "id", * "uuid" = "uuid" - * } + * }, + * menu_base_path = "entity-test/manage/%entity_test" * ) */ class EntityTest extends EntityNG { @@ -74,4 +76,12 @@ public function __construct(array $values, $entity_type) { unset($this->name); unset($this->user_id); } + + /** + * Overrides Drupal\entity\Entity::label(). + */ + public function label($langcode = LANGUAGE_DEFAULT) { + return $this->getTranslation($langcode)->name->value; + } + } diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php index 8aedeee1cdd183590c05298ff3e3a8adb3ba1946..51468af0c747c38af040e2b78fab35af265c4dfa 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php @@ -24,6 +24,7 @@ * form_controller_class = { * "default" = "Drupal\taxonomy\TermFormController" * }, + * translation_controller_class = "Drupal\taxonomy\TermTranslationController", * base_table = "taxonomy_term_data", * uri_callback = "taxonomy_term_uri", * fieldable = TRUE, @@ -41,7 +42,8 @@ * "label" = "Taxonomy term page", * "custom_settings" = FALSE * } - * } + * }, + * menu_base_path = "taxonomy/term/%taxonomy_term" * ) */ class Term extends Entity implements ContentEntityInterface { diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php new file mode 100644 index 0000000000000000000000000000000000000000..2fa4227a98ff77f2f703b2ea1f8631de7964002b --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php @@ -0,0 +1,42 @@ +getSourceLangcode($form_state)) { + $entity = translation_entity_form_controller($form_state)->getEntity($form_state); + // We need a redirect here, otherwise we would get an access denied page + // since the curret URL would be preserved and we would try to add a + // translation for a language that already has a translation. + $form_state['redirect'] = $this->getEditPath($entity); + } + } +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php new file mode 100644 index 0000000000000000000000000000000000000000..907ce879336408364ea73ea43e892e3d1d712387 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php @@ -0,0 +1,89 @@ + 'Taxonomy term translation UI', + 'description' => 'Tests the basic term translation UI.', + 'group' => 'Taxonomy', + ); + } + + /** + * Overrides \Drupal\simpletest\WebTestBase::setUp(). + */ + function setUp() { + $this->entityType = 'taxonomy_term'; + $this->bundle = 'tags'; + $this->name = $this->randomName(); + parent::setUp(); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::setupBundle(). + */ + protected function setupBundle() { + parent::setupBundle(); + + // Create a vocabulary. + $vocabulary = entity_create('taxonomy_vocabulary', array( + 'name' => $this->bundle, + 'description' => $this->randomName(), + 'machine_name' => $this->bundle, + 'langcode' => LANGUAGE_NOT_SPECIFIED, + 'help' => '', + 'weight' => mt_rand(0, 10), + )); + taxonomy_vocabulary_save($vocabulary); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission(). + */ + function getTranslatorPermissions() { + return array('administer taxonomy', "translate $this->entityType entities", 'edit original values'); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::createEntity(). + */ + protected function createEntity($values, $langcode) { + $vocabulary = taxonomy_vocabulary_machine_name_load($this->bundle); + $values['vid'] = $vocabulary->id(); + return parent::createEntity($values, $langcode); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues(). + */ + protected function getNewEntityValues($langcode) { + // Term name is not translatable hence we use a fixed value. + return array('name' => $this->name) + parent::getNewEntityValues($langcode); + } + +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php index f6e51105060655d3f9791c742b070dd10834ff89..c26251e97b212889e5444aed54476993ada9ae6e 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php @@ -98,6 +98,12 @@ protected function actions(array $form, array &$form_state) { array_unshift($actions['submit']['#submit'],'language_configuration_element_submit'); array_unshift($actions['submit']['#submit'], array($this, 'languageConfigurationSubmit')); } + // We cannot leverage the regular submit handler definition because we + // have button-specific ones here. Hence we need to explicitly set it for + // the submit action, otherwise it would be ignored. + if (module_exists('translation_entity')) { + array_unshift($actions['submit']['#submit'], 'translation_entity_language_configuration_element_submit'); + } return $actions; } else { diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index c92ce64a584a94c7242eb6c34f5abb74707c6703..75573a9bcc526e94219404f8241ebe3844637e3e 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -357,6 +357,8 @@ function taxonomy_admin_paths() { $paths = array( 'taxonomy/term/*/edit' => TRUE, 'taxonomy/term/*/delete' => TRUE, + 'taxonomy/term/*/translations' => TRUE, + 'taxonomy/term/*/translations/*' => TRUE, ); return $paths; } diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php new file mode 100644 index 0000000000000000000000000000000000000000..26a26992b47a41a73af0015f2c12f8bd2d75f290 --- /dev/null +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php @@ -0,0 +1,433 @@ +entityType = $entity_type; + $this->entityInfo = $entity_info; + } + + /** + * Implements EntityTranslationControllerInterface::removeTranslation(). + */ + public function removeTranslation(EntityInterface $entity, $langcode) { + $translations = $entity->getTranslationLanguages(); + // @todo Handle properties. + // Remove field translations. + foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + if ($field['translatable']) { + $entity->{$field_name}[$langcode] = array(); + } + } + } + + /** + * Implements EntityTranslationControllerInterface::retranslate(). + */ + public function retranslate(EntityInterface $entity, $langcode = NULL) { + $updated_langcode = !empty($langcode) ? $langcode : $entity->language()->langcode; + $translations = $entity->getTranslationLanguages(); + foreach ($translations as $langcode => $language) { + $entity->retranslate[$langcode] = $langcode != $updated_langcode; + } + } + + /** + * Implements EntityTranslationControllerInterface::getBasePath(). + */ + public function getBasePath(EntityInterface $entity) { + return $this->getPathInstance($this->entityInfo['menu_base_path'], $entity->id()); + } + + /** + * Implements EntityTranslationControllerInterface::getEditPath(). + */ + public function getEditPath(EntityInterface $entity) { + return isset($this->entityInfo['menu_edit_path']) ? $this->getPathInstance($this->entityInfo['menu_edit_path'], $entity->id()) : FALSE; + } + + /** + * Implements EntityTranslationControllerInterface::getViewPath(). + */ + public function getViewPath(EntityInterface $entity) { + return isset($this->entityInfo['menu_view_path']) ? $this->getPathInstance($this->entityInfo['menu_view_path'], $entity->id()) : FALSE; + } + + /** + * Implements EntityTranslationControllerInterface::getAccess(). + */ + public function getAccess(EntityInterface $entity, $op) { + return TRUE; + } + + /** + * Implements EntityTranslationControllerInterface::getTranslationAccess(). + */ + public function getTranslationAccess(EntityInterface $entity, $langcode) { + $entity_type = $entity->entityType(); + return (user_access('translate any entity') || user_access("translate $entity_type entities")) && ($langcode != $entity->language()->langcode || user_access('edit original values')); + } + + /** + * Implements EntityTranslationControllerInterface::getSourceLanguage(). + */ + public function getSourceLangcode(array $form_state) { + return isset($form_state['translation_entity']['source']) ? $form_state['translation_entity']['source']->langcode : FALSE; + } + + /** + * Implements EntityTranslationControllerInterface::entityFormAlter(). + */ + public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity) { + $form_controller = translation_entity_form_controller($form_state); + $form_langcode = $form_controller->getFormLangcode($form_state); + $entity_langcode = $entity->language()->langcode; + $source_langcode = $this->getSourceLangcode($form_state); + + $new_translation = !empty($source_langcode); + $translations = $entity->getTranslationLanguages(); + if ($new_translation) { + // Make sure a new translation does not appear as existing yet. + unset($translations[$form_langcode]); + } + $is_translation = !$form_controller->isDefaultFormLangcode($form_state); + $has_translations = count($translations) > 1; + + // Adjust page title to specify the current language being edited, if we + // have at least one translation. + $languages = language_list(); + if (isset($languages[$form_langcode]) && ($has_translations || $new_translation)) { + $title = $this->entityFormTitle($entity); + // When editing the original values display just the entity label. + if ($form_langcode != $entity->language()->langcode) { + $t_args = array('%language' => $languages[$form_langcode]->name, '%title' => $entity->label()); + $title = empty($source_langcode) ? $title . ' [' . t('%language translation', $t_args) . ']' : t('Create %language translation of %title', $t_args); + } + drupal_set_title($title, PASS_THROUGH); + } + + // Display source language selector only if we are creating a new + // translation and there are at least two translations available. + if ($has_translations && $new_translation) { + $form['source_langcode'] = array( + '#type' => 'fieldset', + '#title' => t('Source language: @language', array('@language' => $languages[$source_langcode]->name)), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + '#weight' => -100, + '#multilingual' => TRUE, + 'source' => array( + '#type' => 'select', + '#default_value' => $source_langcode, + '#options' => array(), + ), + 'submit' => array( + '#type' => 'submit', + '#value' => t('Change'), + '#submit' => array(array($this, 'entityFormSourceChange')), + ), + ); + foreach (language_list(LANGUAGE_CONFIGURABLE) as $language) { + if (isset($translations[$language->langcode])) { + $form['source_langcode']['source']['#options'][$language->langcode] = $language->name; + } + } + } + + // Disable languages for existing translations, so it is not possible to + // switch this node to some language which is already in the translation + // set. + $language_widget = isset($form['langcode']) && $form['langcode']['#type'] == 'language_select'; + if ($language_widget && $has_translations) { + $form['langcode']['#options'] = array(); + foreach (language_list(LANGUAGE_CONFIGURABLE) as $language) { + if (empty($translations[$language->langcode]) || $language->langcode == $entity_langcode) { + $form['langcode']['#options'][$language->langcode] = $language->name; + } + } + } + + if ($is_translation) { + if ($language_widget) { + $form['langcode']['#access'] = FALSE; + } + + // Replace the delete button with the delete translation one. + if (!$new_translation) { + $weight = 100; + foreach (array('delete', 'submit') as $key) { + if (isset($form['actions'][$key]['weight'])) { + $weight = $form['actions'][$key]['weight']; + break; + } + } + $form['actions']['delete_translation'] = array( + '#type' => 'submit', + '#value' => t('Delete translation'), + '#weight' => $weight, + '#submit' => array(array($this, 'entityFormDeleteTranslation')), + ); + } + + // Always remove the delete button on translation forms. + unset($form['actions']['delete']); + } + + // We need to display the translation tab only when there is at least one + // translation available or a new one is about to be created. + if ($new_translation || $has_translations) { + $form['translation'] = array( + '#type' => 'fieldset', + '#title' => t('Translation'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + '#weight' => 10, + '#access' => $this->getTranslationAccess($entity, $form_langcode), + '#multilingual' => TRUE, + ); + + $translate = !$new_translation && $entity->retranslate[$form_langcode]; + if (!$translate) { + $form['translation']['retranslate'] = array( + '#type' => 'checkbox', + '#title' => t('Flag other translations as outdated'), + '#default_value' => FALSE, + '#description' => t('If you made a significant change, which means the other translations should be updated, you can flag all translations of this content as outdated. This will not change any other property of them, like whether they are published or not.'), + ); + } + else { + $form['translation']['translate'] = array( + '#type' => 'checkbox', + '#title' => t('This translation needs to be updated'), + '#default_value' => $translate, + '#description' => t('When this option is checked, this translation needs to be updated. Uncheck when the translation is up to date again.'), + ); + } + + if ($language_widget) { + $form_langcode['#multilingual'] = TRUE; + } + + $form['#process'][] = array($this, 'entityFormSharedElements'); + } + + // Process the submitted values before they are stored. + $form['#entity_builders'][] = array($this, 'entityFormEntityBuild'); + + // Handle entity deletion. + if (isset($form['actions']['delete'])) { + $form['actions']['delete']['#submit'][] = array($this, 'entityFormDelete'); + } + } + + /** + * Process callback: Determines which elements get clue in the form. + * + * @param array $element + * Form API element. + * + * @return array + * A processed element with the shared elements marked with a clue. + * + * @see \Drupal\translation_entity\EntityTranslationController::entityFormAlter() + */ + public function entityFormSharedElements($element) { + static $ignored_types; + + // @todo Find a more reliable way to determine if a form element concerns a + // multilingual value. + if (!isset($ignored_types)) { + $ignored_types = array_flip(array('actions', 'value', 'hidden', 'vertical_tabs', 'token')); + } + + foreach (element_children($element) as $key) { + if (!isset($element[$key]['#type'])) { + $this->entityFormSharedElements($element[$key]); + } + else { + // Ignore non-widget form elements. + if (isset($ignored_types[$element[$key]['#type']])) { + continue; + } + // Elements are considered to be non multilingual by default. + if (empty($element[$key]['#multilingual'])) { + $this->addTranslatabilityClue($element[$key]); + } + } + } + + return $element; + } + + /** + * Adds a clue about the form element translatability. + * + * If the given element does not have a #title attribute, the function is + * recursively applied to child elements. + * + * @param array $element + * A form element array. + */ + protected function addTranslatabilityClue(&$element) { + static $suffix, $fapi_title_elements; + + // Elements which can have a #title attribute according to FAPI Reference. + if (!isset($suffix)) { + $suffix = ' (' . t('all languages') . ')'; + $fapi_title_elements = array_flip(array('checkbox', 'checkboxes', 'date', 'fieldset', 'file', 'item', 'password', 'password_confirm', 'radio', 'radios', 'select', 'text_format', 'textarea', 'textfield', 'weight')); + } + + // Update #title attribute for all elements that are allowed to have a + // #title attribute according to the Form API Reference. The reason for this + // check is because some elements have a #title attribute even though it is + // not rendered, e.g. field containers. + if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) { + $element['#title'] .= $suffix; + } + // If the current element does not have a (valid) title, try child elements. + elseif ($children = element_children($element)) { + foreach ($children as $delta) { + $this->addTranslatabilityClue($element[$delta], $suffix); + } + } + // If there are no children, fall back to the current #title attribute if it + // exists. + elseif (isset($element['#title'])) { + $element['#title'] .= $suffix; + } + } + + /** + * Entity builder method. + * + * @param string $entity_type + * The type of the entity. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose form is being built. + * + * @see \Drupal\translation_entity\EntityTranslationController::entityFormAlter() + */ + public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, array &$form_state) { + $form_controller = translation_entity_form_controller($form_state); + $form_langcode = $form_controller->getFormLangcode($form_state); + $source_langcode = $this->getSourceLangcode($form_state); + + if ($source_langcode) { + // @todo Use the entity setter when all entities support multilingual + // properties. + $entity->source[$form_langcode] = $source_langcode; + } + + // Ensure every key has at least a default value. Subclasses may provide + // entity-specific values to alter them. + $values = isset($form_state['values']['translation']) ? $form_state['values']['translation'] : array(); + $entity->retranslate[$form_langcode] = isset($values['translate']) && $values['translate']; + + if (!empty($values['retranslate'])) { + $this->retranslate($entity, $form_langcode); + } + } + + /** + * Form submission handler for EntityTranslationController::entityFormAlter(). + * + * Takes care of the source language change. + */ + public function entityFormSourceChange($form, &$form_state) { + $form_controller = translation_entity_form_controller($form_state); + $entity = $form_controller->getEntity($form_state); + $source = $form_state['values']['source_langcode']['source']; + $path = $this->getBasePath($entity) . '/translations/add/' . $source . '/' . $form_controller->getFormLangcode($form_state); + $form_state['redirect'] = array('path' => $path); + $languages = language_list(); + drupal_set_message(t('Source language set to: %language', array('%language' => $languages[$source]->name))); + } + + /** + * Form submission handler for EntityTranslationController::entityFormAlter(). + * + * Takes care of entity deletion. + */ + function entityFormDelete($form, &$form_state) { + $form_controller = translation_entity_form_controller($form_state); + $entity = $form_controller->getEntity($form_state); + if (count($entity->getTranslationLanguages()) > 1) { + drupal_set_message(t('This will delete all the translations of %label.', array('%label' => $entity->label())), 'warning'); + } + } + + /** + * Form submission handler for EntityTranslationController::entityFormAlter(). + * + * Takes care of entity translation deletion. + */ + function entityFormDeleteTranslation($form, &$form_state) { + $form_controller = translation_entity_form_controller($form_state); + $entity = $form_controller->getEntity($form_state); + $base_path = $this->getBasePath($entity); + $form_langcode = $form_controller->getFormLangcode($form_state); + $form_state['redirect'] = $base_path . '/translations/delete/' . $form_langcode; + } + + /** + * Returns the title to be used for the entity form page. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose form is being altered. + */ + protected function entityFormTitle(EntityInterface $entity) { + return $entity->label(); + } + + /** + * Returns an instance of the given path. + * + * @param $path + * An internal path containing the entity id wildcard. + * + * @return string + * The instantiated path. + */ + protected function getPathInstance($path, $entity_id) { + $wildcard = $this->entityInfo['menu_path_wildcard']; + return str_replace($wildcard, $entity_id, $path); + } +} diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..ed08fc08c6ab2b0c0a22166b0547325979beaa61 --- /dev/null +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php @@ -0,0 +1,190 @@ + 'mymodule/myentity/%my_entity_loader', + * 'menu_path_wildcard' => '%my_entity_loader', + * 'translation_controller_class' => 'Drupal\mymodule\MyEntityTranslationController', + * 'translation' => array( + * 'translation_entity' => array( + * 'access_callback' => 'mymodule_myentity_translate_access', + * 'access_arguments' => array(2), + * ), + * ), + * ); + * } + * @endcode + * + * @see \Drupal\Core\Entity\EntityManager + */ +interface EntityTranslationControllerInterface { + + /** + * Returns the base path for the current entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to the path should refer to. + * + * @return string + * The entity base path. + */ + public function getBasePath(EntityInterface $entity); + + /** + * Returns the path of the entity edit form. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to the path should refer to. + * + * @return string + * The entity edit path. + */ + public function getEditPath(EntityInterface $entity); + + /** + * Returns the path of the entity view page. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to the path should refer to. + * + * @return string + * The entity view path. + */ + public function getViewPath(EntityInterface $entity); + + /** + * Checks if the user can perform the given operation on the wrapped entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity access should be checked for. + * @param string $op + * The operation to be performed. Possible values are: + * - "view" + * - "update" + * - "delete" + * - "create" + * + * @return + * TRUE if the user is allowed to perform the given operation, FALSE + * otherwise. + */ + public function getAccess(EntityInterface $entity, $op); + + /** + * Checks if a user is allowed to edit the given translation. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose translation has to be accessed. + * @param string $langcode + * The language code identifying the translation to be accessed. + * + * @return boolean + * TRUE if the operation may be performed, FALSE otherwise. + */ + public function getTranslationAccess(EntityInterface $entity, $langcode); + + /** + * Retrieves the source language for the translation being created. + * + * @param array $form_state + * The form state array. + * + * @return string + * The source language code. + */ + public function getSourceLangcode(array $form_state); + + /** + * Removes the translation values from the given entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose values should be removed. + * @param string $langcode + * The language code identifying the translation being deleted. + */ + public function removeTranslation(EntityInterface $entity, $langcode); + + /** + * Marks translations as outdated. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being translated. + * @param string $langcode + * (optional) The language code of the updated language: all the other + * translations will be marked as outdated. Defaults to the entity language. + */ + public function retranslate(EntityInterface $entity, $langcode = NULL); + + /** + * Performs the needed alterations to the entity form. + * + * @param array $form + * The entity form to be altered to provide the translation workflow. + * @param array $form_state + * The form state array. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being created or edited. + */ + public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity); +} diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTestTranslationUITest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTestTranslationUITest.php new file mode 100644 index 0000000000000000000000000000000000000000..6f0f9c0ee27688e511e49a8dd73523fb7f1fad1a --- /dev/null +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTestTranslationUITest.php @@ -0,0 +1,55 @@ + 'Entity Test Translation UI', + 'description' => 'Tests the test entity translation UI.', + 'group' => 'Entity Translation UI', + ); + } + + /** + * Overrides \Drupal\simpletest\WebTestBase::setUp(). + */ + function setUp() { + $this->entityType = 'entity_test'; + parent::setUp(); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission(). + */ + function getTranslatorPermissions() { + return array('administer entity_test content', "translate $this->entityType entities", 'edit original values'); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues(). + */ + protected function getNewEntityValues($langcode) { + return array( + 'name' => $this->randomName(), + 'user_id' => mt_rand(1, 128), + ) + parent::getNewEntityValues($langcode); + } + +} diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php new file mode 100644 index 0000000000000000000000000000000000000000..934ed628587712e93252217ac196e30d0d01d845 --- /dev/null +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php @@ -0,0 +1,317 @@ +setupLanguages(); + $this->setupBundle(); + $this->enableTranslation(); + $this->setupTranslator(); + $this->setupTestFields(); + } + + /** + * Enables additional languages. + */ + protected function setupLanguages() { + $this->langcodes = array('it', 'fr'); + foreach ($this->langcodes as $langcode) { + language_save(new Language(array('langcode' => $langcode))); + } + array_unshift($this->langcodes, language_default()->langcode); + } + + /** + * Creates or initializes the bundle date if needed. + */ + protected function setupBundle() { + if (empty($this->bundle)) { + $this->bundle = $this->entityType; + } + } + + /** + * Enables translation for the current entity type and bundle. + */ + protected function enableTranslation() { + // Enable translation for the current entity type and ensure the change is + // picked up. + translation_entity_set_config($this->entityType, $this->bundle, 'enabled', TRUE); + drupal_static_reset(); + entity_info_cache_clear(); + menu_router_rebuild(); + } + + /** + * Returns an array of permissions needed for the translator. + */ + abstract function getTranslatorPermissions(); + + /** + * Creates and activates a translator user. + */ + protected function setupTranslator() { + $translator = $this->drupalCreateUser($this->getTranslatorPermissions()); + $this->drupalLogin($translator); + } + + /** + * Creates the test fields. + */ + protected function setupTestFields() { + $this->fieldName = 'field_test_et_ui_test'; + + $field = array( + 'field_name' => $this->fieldName, + 'type' => 'text', + 'cardinality' => 1, + 'translatable' => TRUE, + ); + field_create_field($field); + + $instance = array( + 'entity_type' => $this->entityType, + 'field_name' => $this->fieldName, + 'bundle' => $this->bundle, + 'label' => 'Test translatable text-field', + 'widget' => array( + 'type' => 'text_textfield', + 'weight' => 0, + ), + ); + field_create_instance($instance); + } + + /** + * Tests the basic translation UI. + */ + function testTranslationUI() { + // Create a new test entity with original values in the default language. + $default_langcode = $this->langcodes[0]; + $values[$default_langcode] = $this->getNewEntityValues($default_langcode); + $id = $this->createEntity($values[$default_langcode], $default_langcode); + $entity = entity_load($this->entityType, $id, TRUE); + $this->assertTrue($entity, t('Entity found in the database.')); + + $translation = $this->getTranslation($entity, $default_langcode); + foreach ($values[$default_langcode] as $property => $value) { + $stored_value = $this->getValue($translation, $property, $default_langcode); + $value = is_array($value) ? $value[0]['value'] : $value; + $message = format_string('@property correctly stored in the default language.', array('@property' => $property)); + $this->assertIdentical($stored_value, $value, $message); + } + + // Add an entity translation. + $langcode = 'it'; + $values[$langcode] = $this->getNewEntityValues($langcode); + + $controller = translation_entity_controller($this->entityType); + $base_path = $controller->getBasePath($entity); + $path = $langcode . '/' . $base_path . '/translations/add/' . $default_langcode . '/' . $langcode; + $this->drupalPost($path, $this->getEditValues($values, $langcode), t('Save')); + if ($this->testLanguageSelector) { + $this->assertNoFieldByXPath('//select[@id="edit-langcode"]', NULL, 'Language selector correclty disabled on translations.'); + } + $entity = entity_load($this->entityType, $entity->id(), TRUE); + + // Switch the source language. + $langcode = 'fr'; + $source_langcode = 'it'; + $edit = array('source_langcode[source]' => $source_langcode); + $path = $langcode . '/' . $base_path . '/translations/add/' . $default_langcode . '/' . $langcode; + $this->drupalPost($path, $edit, t('Change')); + $this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[fr][0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.'); + + // Add another translation and mark the other ones as outdated. + $values[$langcode] = $this->getNewEntityValues($langcode); + $edit = $this->getEditValues($values, $langcode) + array('translation[retranslate]' => TRUE); + $this->drupalPost($path, $edit, t('Save')); + $entity = entity_load($this->entityType, $entity->id(), TRUE); + + // Check that the entered values have been correctly stored. + foreach ($values as $langcode => $property_values) { + $translation = $this->getTranslation($entity, $langcode); + foreach ($property_values as $property => $value) { + $stored_value = $this->getValue($translation, $property, $langcode); + $value = is_array($value) ? $value[0]['value'] : $value; + $message = format_string('%property correctly stored with language %language.', array('%property' => $property, '%language' => $langcode)); + $this->assertEqual($stored_value, $value, $message); + } + } + + // Check that every translation has the correct "outdated" status. + foreach ($this->langcodes as $enabled_langcode) { + $prefix = $enabled_langcode != $default_langcode ? $enabled_langcode . '/' : ''; + $path = $prefix . $controller->getEditPath($entity); + $this->drupalGet($path); + if ($enabled_langcode == $langcode) { + $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.'); + } + else { + $this->assertFieldByXPath('//input[@name="translation[translate]"]', TRUE, 'The translate flag is checked by default.'); + $edit = array('translation[translate]' => FALSE); + $this->drupalPost($path, $edit, t('Save')); + $this->drupalGet($path); + $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.'); + $entity = entity_load($this->entityType, $entity->id(), TRUE); + $this->assertFalse($entity->retranslate[$enabled_langcode], 'The "outdated" status has been correctly stored.'); + } + } + + // Confirm and delete a translation. + $this->drupalPost($path, array(), t('Delete translation')); + $this->drupalPost(NULL, array(), t('Delete')); + $entity = entity_load($this->entityType, $entity->id(), TRUE); + $translations = $entity->getTranslationLanguages(); + $this->assertTrue(count($translations) == 2 && empty($translations[$enabled_langcode]), 'Translation successfully deleted.'); + } + + /** + * Creates the entity to be translated. + * + * @param array $values + * An array of initial values for the entity. + * @param string $langcode + * The initial language code of the entity. + * + * @return + * The entity id. + */ + protected function createEntity($values, $langcode) { + $entity_values = $values; + $entity_values['langcode'] = $langcode; + $info = entity_get_info($this->entityType); + if (!empty($info['entity_keys']['bundle'])) { + $entity_values[$info['entity_keys']['bundle']] = $this->bundle; + } + $controller = entity_get_controller($this->entityType); + if (!($controller instanceof DatabaseStorageControllerNG)) { + foreach ($values as $property => $value) { + if (is_array($value)) { + $entity_values[$property] = array($langcode => $value); + } + } + } + $entity = entity_create($this->entityType, $entity_values); + $entity->save(); + return $entity->id(); + } + + /** + * Returns an array of entity field values to be tested. + */ + protected function getNewEntityValues($langcode) { + return array($this->fieldName => array(array('value' => $this->randomName(16)))); + } + + /** + * Returns an edit array containing the values to be posted. + */ + protected function getEditValues($values, $langcode, $new = FALSE) { + $edit = $values[$langcode]; + $langcode = $new ? LANGUAGE_NOT_SPECIFIED : $langcode; + foreach ($values[$langcode] as $property => $value) { + if (is_array($value)) { + $edit["{$property}[$langcode][0][value]"] = $value[0]['value']; + unset($edit[$property]); + } + } + return $edit; + } + + /** + * Returns the translation object to use to retrieve the translated values. + * + * @param \Drupal\Core\Enitity\EntityInterface $entity + * The entity being tested. + * @param string $langcode + * The language code identifying the translation to be retrieved. + * + * @return \Drupal\Core\TypedData\TranslatableInterface + * The translation object to act on. + */ + protected function getTranslation(EntityInterface $entity, $langcode) { + return $entity instanceof EntityNG ? $entity->getTranslation($langcode) : $entity; + } + + /** + * Returns the value for the specified property in the given language. + * + * @param \Drupal\Core\TypedData\TranslatableInterface $translation + * The translation object the property value should be retrieved from. + * @param string $property + * The property name. + * @param string $langcode + * The property value. + * + * @return + * The property value. + */ + protected function getValue(ComplexDataInterface $translation, $property, $langcode) { + if (($translation instanceof EntityInterface) && !($translation instanceof EntityNG)) { + return is_array($translation->$property) ? $translation->{$property}[$langcode][0]['value'] : $translation->$property; + } + else { + return $translation->get($property)->value; + } + } + +} diff --git a/core/modules/translation_entity/translation_entity.admin.inc b/core/modules/translation_entity/translation_entity.admin.inc new file mode 100644 index 0000000000000000000000000000000000000000..bacba11a8cc4e18664c4995ca722f54174b52791 --- /dev/null +++ b/core/modules/translation_entity/translation_entity.admin.inc @@ -0,0 +1,262 @@ + $field_name); + + $warning = t('By submitting this form these changes will apply to the %name field everywhere it is used.', $t_args); + if ($field['translatable']) { + $title = t('Are you sure you want to disable translation for the %name field?', $t_args); + $warning .= "
" . t("All the existing translations of this field will be deleted.
This action cannot be undone."); + } + else { + $title = t('Are you sure you want to enable translation for the %name field?', $t_args); + } + + // We need to keep some information for later processing. + $form_state['field'] = $field; + + // Store the 'translatable' status on the client side to prevent outdated form + // submits from toggling translatability. + $form['translatable'] = array( + '#type' => 'hidden', + '#default_value' => $field['translatable'], + ); + + return confirm_form($form, $title, '', $warning); +} + +/** + * Form submission handler for translation_entity_translatable_form(). + * + * This submit handler maintains consistency between the translatability of an + * entity and the language under which the field data is stored. When a field is + * marked as translatable, all the data in + * $entity->{field_name}[LANGUAGE_NOT_SPECIFIED] is moved to + * $entity->{field_name}[$entity_language]. When a field is marked as + * untranslatable the opposite process occurs. Note that marking a field as + * untranslatable will cause all of its translations to be permanently removed, + * with the exception of the one corresponding to the entity language. + */ +function translation_entity_translatable_form_submit(array $form, array $form_state) { + // This is the current state that we want to reverse. + $translatable = $form_state['values']['translatable']; + $field_name = $form_state['field']['field_name']; + $field = field_info_field($field_name); + + if ($field['translatable'] !== $translatable) { + // Field translatability has changed since form creation, abort. + $t_args = array('%field_name'); + $msg = $translatable ? + t('The field %field_name is already translatable. No change was performed.', $t_args): + t('The field %field_name is already untranslatable. No change was performed.', $t_args); + drupal_set_message($msg, 'warning'); + return; + } + + // If a field is untranslatable, it can have no data except under + // LANGUAGE_NOT_SPECIFIED. Thus we need a field to be translatable before we convert + // data to the entity language. Conversely we need to switch data back to + // LANGUAGE_NOT_SPECIFIED before making a field untranslatable lest we lose + // information. + $operations = array( + array('translation_entity_translatable_batch', array(!$translatable, $field_name)), + array('translation_entity_translatable_switch', array(!$translatable, $field_name)), + ); + $operations = $translatable ? $operations : array_reverse($operations); + + $t_args = array('%field' => $field_name); + $title = !$translatable ? t('Enabling translation for the %field field', $t_args) : t('Disabling translation for the %field field', $t_args); + + $batch = array( + 'title' => $title, + 'operations' => $operations, + 'finished' => 'translation_entity_translatable_batch_done', + 'file' => drupal_get_path('module', 'translation_entity') . '/translation_entity.admin.inc', + ); + + batch_set($batch); +} + +/** + * Toggles translatability of the given field. + * + * This is called from a batch operation, but should only run once per field. + * + * @param bool $translatable + * Indicator of whether the field should be made translatable (TRUE) or + * untranslatble (FALSE). + * @param string $field_name + * Field machine name. + */ +function translation_entity_translatable_switch($translatable, $field_name) { + $field = field_info_field($field_name); + + if ($field['translatable'] === $translatable) { + return; + } + + $field['translatable'] = $translatable; + field_update_field($field); +} + +/** + * Batch callback: Converts field data to or from LANGUAGE_NOT_SPECIFIED. + * + * @param bool $translatable + * Indicator of whether the field should be made translatable (TRUE) or + * untranslatble (FALSE). + * @param string $field_name + * Field machine name. + */ +function translation_entity_translatable_batch($translatable, $field_name, &$context) { + $entity_types = array(); + + // Determine the entity types to act on. + foreach (field_info_instances() as $entity_type => $info) { + foreach ($info as $bundle => $instances) { + foreach ($instances as $instance_field_name => $instance) { + if ($instance_field_name == $field_name) { + $entity_types[] = $entity_type; + break 2; + } + } + } + } + + if (empty($context['sandbox'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = 0; + + foreach ($entity_types as $entity_type) { + // How many entities will need processing? + $query = entity_query($entity_type); + $count = $query + ->exists($field_name) + ->count() + ->execute(); + + $context['sandbox']['max'] += $count; + $context['sandbox']['progress_entity_type'][$entity_type] = 0; + $context['sandbox']['max_entity_type'][$entity_type] = $count; + } + + if ($context['sandbox']['max'] === 0) { + // Nothing to do. + $context['finished'] = 1; + return; + } + } + + foreach ($entity_types as $entity_type) { + if ($context['sandbox']['max_entity_type'][$entity_type] === 0) { + continue; + } + + $info = entity_get_info($entity_type); + $offset = $context['sandbox']['progress_entity_type'][$entity_type]; + $query = entity_query($entity_type); + $result = $query + ->exists($field_name) + ->sort($info['entity_keys']['id']) + ->range($offset, 10) + ->execute(); + + foreach (entity_load_multiple($entity_type, $result) as $id => $entity) { + $context['sandbox']['max_entity_type'][$entity_type] -= count($result); + $context['sandbox']['progress_entity_type'][$entity_type]++; + $context['sandbox']['progress']++; + $langcode = $entity->language()->langcode; + + // Skip process for language neutral entities. + if ($langcode == LANGUAGE_NOT_SPECIFIED) { + continue; + } + + // We need a two-step approach while updating field translations: given + // that field-specific update functions might rely on the stored values to + // perform their processing, see for instance file_field_update(), first + // we need to store the new translations and only after we can remove the + // old ones. Otherwise we might have data loss, since the removal of the + // old translations might occur before the new ones are stored. + if ($translatable && isset($entity->{$field_name}[LANGUAGE_NOT_SPECIFIED])) { + // If the field is being switched to translatable and has data for + // LANGUAGE_NOT_SPECIFIED then we need to move the data to the right + // language. + $entity->{$field_name}[$langcode] = $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED]; + // Store the original value. + _translation_entity_update_field($entity_type, $entity, $field_name); + $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED] = array(); + // Remove the language neutral value. + _translation_entity_update_field($entity_type, $entity, $field_name); + } + elseif (!$translatable && isset($entity->{$field_name}[$langcode])) { + // The field has been marked untranslatable and has data in the entity + // language: we need to move it to LANGUAGE_NOT_SPECIFIED and drop the + // other translations. + $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED] = $entity->{$field_name}[$langcode]; + // Store the original value. + _translation_entity_update_field($entity_type, $entity, $field_name); + // Remove translations. + foreach ($entity->{$field_name} as $langcode => $items) { + if ($langcode != LANGUAGE_NOT_SPECIFIED) { + $entity->{$field_name}[$langcode] = array(); + } + } + _translation_entity_update_field($entity_type, $entity, $field_name); + } + else { + // No need to save unchanged entities. + continue; + } + } + } + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; +} + +/** + * Stores the given field translations. + */ +function _translation_entity_update_field($entity_type, EntityInterface $entity, $field_name) { + $empty = 0; + $field = field_info_field($field_name); + + // Ensure that we are trying to store only valid data. + foreach ($entity->{$field_name} as $langcode => $items) { + $entity->{$field_name}[$langcode] = _field_filter_items($field, $entity->{$field_name}[$langcode]); + $empty += empty($entity->{$field_name}[$langcode]); + } + + // Save the field value only if there is at least one item available, + // otherwise any stored empty field value would be deleted. If this happens + // the range queries would be messed up. + if ($empty < count($entity->{$field_name})) { + field_attach_presave($entity_type, $entity); + field_attach_update($entity_type, $entity); + } +} + +/** + * Batch finished callback: Checks the exit status of the batch operation. + */ +function translation_entity_translatable_batch_done($success, $results, $operations) { + if ($success) { + drupal_set_message(t("Successfully changed field translation setting.")); + } + else { + // @todo: Do something about this case. + drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."), 'error'); + } +} + diff --git a/core/modules/translation_entity/translation_entity.info b/core/modules/translation_entity/translation_entity.info new file mode 100644 index 0000000000000000000000000000000000000000..4a28def9ce35649b2d8be8d0959892986b8ffd8b --- /dev/null +++ b/core/modules/translation_entity/translation_entity.info @@ -0,0 +1,6 @@ +name = Entity Translation +description = Allows entities to be translated into different languages. +dependencies[] = language +package = Core +version = VERSION +core = 8.x diff --git a/core/modules/translation_entity/translation_entity.install b/core/modules/translation_entity/translation_entity.install new file mode 100644 index 0000000000000000000000000000000000000000..b571da381b852fad26366f1b4217936bc4f69b56 --- /dev/null +++ b/core/modules/translation_entity/translation_entity.install @@ -0,0 +1,71 @@ + 'Table to track entity translations', + 'fields' => array( + 'entity_type' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The entity type this translation relates to', + ), + 'entity_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The entity id this translation relates to', + ), + 'langcode' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The target language for this translation.', + ), + 'source' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The source language from which this translation was created.', + ), + 'translate' => array( + 'description' => 'A boolean indicating whether this translation needs to be updated.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('entity_type', 'entity_id', 'langcode'), + ); + return $schema; +} + +/** + * Implements hook_install(). + */ +function translation_entity_install() { + language_negotiation_include(); + language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LANGUAGE_NEGOTIATION_URL => 0)); +} + +/** + * Implements hook_enable(). + */ +function translation_entity_enable() { + $t_args = array( + '!language_url' => url('admin/config/regional/language'), + ); + $message = t('You just added content translation capabilities to your site. To exploit them be sure to enable at least two languages and enable translation for content types, taxonomy vocabularies, accounts and any other element whose content you wish to translate.', $t_args); + drupal_set_message($message, 'warning'); +} diff --git a/core/modules/translation_entity/translation_entity.module b/core/modules/translation_entity/translation_entity.module new file mode 100644 index 0000000000000000000000000000000000000000..b0c852673da2fc408c5e310b7c0063171637d33e --- /dev/null +++ b/core/modules/translation_entity/translation_entity.module @@ -0,0 +1,702 @@ +' . t('About') . ''; + $output .= '

' . t('The Entity Translation module allows you to create and manage translations for your Drupal site content. You can specify which elements need to be translated at the content-type level for content items and comments, at the vocabulary level for taxonomy terms, and at the site level for user accounts. Other modules may provide additional elements that can be translated. For more information, see the online handbook entry for Entity Translation.', array('!url' => 'http://drupal.org/documentation/modules/entity_translation')) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Enabling translation') . '
'; + $output .= '

' . t('Before you can translate content, there must be at least two non-system languages added on the languages administration page.', array('!url' => url('admin/config/regional/language'))) . '

'; + $output .= '

' . t('After adding languages, enable translation for any content you wish to translate:') . '

'; + $output .= ''; + $output .= '

' . t('Finally, under the Manage fields tab, edit each field you wish to be translatable, and enable translation under Global settings.') . '

'; + $output .= '
' . t('Translating content') . '
'; + $output .= '
' . t('After enabling translation you can create a new piece of content, or edit existing content and assign it a language. Then, you will see a Translations tab or link that will gives an overview of the translation status for the current content. From there, you can add translations and edit or delete existing translations. This process is similar for every translatable element on your site, such as taxonomy terms, comments or user accounts.') . '
'; + $output .= '
' . t('Changing source language') . '
'; + $output .= '
' . t('When there are two or more possible source languages, selecting a Source language will repopulate the form using the specified source\'s values. For example, French is much closer to Spanish than to Chinese, so changing the French translation\'s source language to Spanish can assist translators.') . '
'; + $output .= '
' . t('Maintaining translations') . '
'; + $output .= '
' . t('If editing content in one language requires that translated versions also be updated to reflect the change, use the Flag other translations as outdated check box to mark the translations as outdated and in need of revision.') . '
'; + $output .= '
' . t('Translation permissions') . '
'; + $output .= '
' . t('The Entity Translation module makes a basic set of permissions available. Additional permissions are made available after translation is enabled for each translatable element.', array('@permissions' => url('admin/people/permissions', array('fragment' => 'module-translation_entity')))) . '
'; + $output .= '
'; + return $output; + } +} + +/** + * Implements hook_language_type_info_alter(). + */ +function translation_entity_language_types_info_alter(array &$language_types) { + unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']); +} + +/** + * Implements hook_entity_info_alter(). + */ +function translation_entity_entity_info_alter(array &$entity_info) { + $edit_form_info = array(); + + // Provide defaults for translation info. + foreach ($entity_info as $entity_type => &$info) { + if (!isset($info['translation']['translation_entity'])) { + $info['translation']['translation_entity'] = array(); + } + + // Every fieldable entity type must have a translation controller class, no + // matter if it is enabled for translation or not. As a matter of fact we + // might need it to correctly switch field translatability when a field is + // shared accross different entities. + $info += array('translation_controller_class' => 'Drupal\translation_entity\EntityTranslationController'); + + // Check whether translation is enabled at least for one bundle. We cannot + // use translation_entity_enabled() here since it would cause infinite + // recursion, as it relies on entity info. + $enabled = FALSE; + $bundles = isset($info['bundles']) ? array_keys($info['bundles']) : array($entity_type); + foreach ($bundles as $bundle) { + if (translation_entity_get_config($entity_type, $bundle, 'enabled')) { + $enabled = TRUE; + break; + } + } + + if ($enabled) { + // If no menu base path is provided we default to the usual + // "entity_type/%entity_type" pattern. + if (!isset($info['menu_base_path'])) { + $path = "$entity_type/%$entity_type"; + $info['menu_base_path'] = $path; + } + + $path = $info['menu_base_path']; + + $info += array( + 'menu_view_path' => $path, + 'menu_edit_path' => "$path/edit", + 'menu_path_wildcard' => "%$entity_type", + ); + + $entity_position = count(explode('/', $path)) - 1; + $info['translation']['translation_entity'] += array( + 'access_callback' => 'translation_entity_translate_access', + 'access_arguments' => array($entity_position), + ); + } + } +} + +/** + * Implements hook_menu(). + */ +function translation_entity_menu() { + $items = array(); + + // Create tabs for all possible entity types. + foreach (entity_get_info() as $entity_type => $info) { + // Provide the translation UI only for enabled types. + if (translation_entity_enabled($entity_type)) { + $path = $info['menu_base_path']; + $entity_position = count(explode('/', $path)) - 1; + $keys = array_flip(array('theme_callback', 'theme_arguments', 'access_callback', 'access_arguments', 'load_arguments')); + $menu_info = array_intersect_key($info['translation']['translation_entity'], $keys) + array('file' => 'translation_entity.pages.inc'); + $item = array(); + + // Plugin annotations cannot contain spaces, thus we need to restore them + // from underscores. + foreach ($menu_info as $key => $value) { + $item[str_replace('_', ' ', $key)] = $value; + } + + $items["$path/translations"] = array( + 'title' => 'Translations', + 'page callback' => 'translation_entity_overview', + 'page arguments' => array($entity_position), + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + ) + $item; + + // Add translation callback. + // @todo Add the access callback instead of replacing it as soon as the + // routing system supports multiple callbacks. + $add_path = "$path/translations/add/%language/%language"; + $language_position = $entity_position + 3; + $args = array($entity_position, $language_position, $language_position + 1); + $items[$add_path] = array( + 'title' => 'Add', + 'page callback' => 'translation_entity_add_page', + 'page arguments' => $args, + 'access callback' => 'translation_entity_add_access', + 'access arguments' => $args, + ) + $item; + + // Delete translation callback. + $items["$path/translations/delete/%language"] = array( + 'title' => 'Delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('translation_entity_delete_confirm', $entity_position, $language_position), + ) + $item; + } + } + + $items['admin/config/regional/translation_entity/translatable/%'] = array( + 'title' => 'Confirm change in translatability.', + 'description' => 'Confirm page for changing field translatability.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('translation_entity_translatable_form', 5), + 'access arguments' => array('toggle field translatability'), + 'file' => 'translation_entity.admin.inc', + ); + + return $items; +} + +/** + * Implements hook_menu_alter(). + */ +function translation_entity_menu_alter(array &$items) { + // Some menu loaders in the item paths might have been altered: we need to + // replace any menu loader with a plain % to check if base paths are still + // compatible. + $paths = array(); + $regex = '|%[^/]+|'; + foreach ($items as $path => $item) { + $path = preg_replace($regex, '%', $path); + $paths[$path] = $path; + } + + // Check that the declared menu base paths are actually valid. + foreach (entity_get_info() as $entity_type => $info) { + if (translation_entity_enabled($entity_type)) { + $path = $info['menu_base_path']; + + // If the base path is not defined or is not compatible with any defined + // one we cannot provide the translation UI for this entity type. + if (!isset($paths[preg_replace($regex, '%', $path)])) { + drupal_set_message(t('The entities of type %entity_type do not define a valid base path: it will not be possible to translate them.', array('%entity_type' => $info['label'])), 'warning'); + unset( + $items["$path/translations"], + $items["$path/translations/add/%language"], + $items["$path/translations/delete/%language"] + ); + } + else { + $entity_position = count(explode('/', $path)) - 1; + $edit_path = $info['menu_edit_path']; + + if (isset($items[$edit_path])) { + // If the edit path is a default local task we need to find the parent + // item. + $edit_path_split = explode('/', $edit_path); + do { + $entity_form_item = &$items[implode('/', $edit_path_split)]; + array_pop($edit_path_split); + } + while (!empty($entity_form_item['type']) && $entity_form_item['type'] == MENU_DEFAULT_LOCAL_TASK); + + // Make the "Translate" tab follow the "Edit" one when possibile. + if (isset($entity_form_item['weight'])) { + $items["$path/translations"]['weight'] = $entity_form_item['weight'] + 0.01; + } + } + } + } + } +} + +/** + * Access callback for the translation overview page. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose translation overview should be displayed. + */ +function translation_entity_translate_access(EntityInterface $entity) { + $entity_type = $entity->entityType(); + return empty($entity->language()->locked) && language_multilingual() && translation_entity_enabled($entity_type, $entity->bundle()) && (user_access('translate any entity') || user_access("translate $entity_type entities")); +} + +/** + * Access callback for the translation addition page. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being translated. + * @param \Drupal\Core\Language\Language $source + * The language of the values being translated. + * @param \Drupal\Core\Language\Language $target + * The language of the translated values. + */ +function translation_entity_add_access(EntityInterface $entity, Language $source = NULL, Language $target = NULL) { + $source = !empty($source) ? $source : $entity->language(); + $target = !empty($target) ? $target : language(LANGUAGE_TYPE_CONTENT); + $translations = $entity->getTranslationLanguages(); + $languages = language_list(); + return $source->langcode != $target->langcode && isset($languages[$source->langcode]) && isset($languages[$target->langcode]) && !isset($translations[$target->langcode]) && translation_entity_access($entity, $target->langcode); +} + +/** + * Returns the key name used to store the configuration item. + * + * Based on the entity type and bundle, the variables used to store the + * configuration will have a common root name. + * + * @param string $entity_type + * The type of the entity the setting refers to. + * @param string $bundle + * The bundle of the entity the setting refers to. + * @param string $setting + * The name of the setting. + * + * @return string + * The key name of the configuration item. + * + * @todo Generalize this logic so that it is available to any module needing + * per-bundle configuration. + */ +function translation_entity_get_config_key($entity_type, $bundle, $setting) { + $entity_type = preg_replace('/[^0-9a-zA-Z_]/', "_", $entity_type); + $bundle = preg_replace('/[^0-9a-zA-Z_]/', "_", $bundle); + return $entity_type . '.' . $bundle . '.translation_entity.' . $setting; +} + +/** + * Retrieves the value for the specified setting. + * + * @param string $entity_type + * The type of the entity the setting refer to. + * @param string $bundle + * The bundle of the entity the setting refer to. + * @param string $setting + * The name of the setting. + * + * @returns mixed + * The stored value for the given setting. + */ +function translation_entity_get_config($entity_type, $bundle, $setting) { + $key = translation_entity_get_config_key($entity_type, $bundle, $setting); + return config('translation_entity.settings')->get($key); +} + +/** + * Stores the given value for the specified setting. + * + * @param string $entity_type + * The type of the entity the setting refer to. + * @param string $bundle + * The bundle of the entity the setting refer to. + * @param string $setting + * The name of the setting. + * @param $value + * The value to be stored for the given setting. + */ +function translation_entity_set_config($entity_type, $bundle, $setting, $value) { + $key = translation_entity_get_config_key($entity_type, $bundle, $setting); + return config('translation_entity.settings')->set($key, $value)->save(); +} + +/** + * Determines whether the given entity type is translatable. + * + * @param string $entity_type + * The type of the entity. + * @param string $bundle + * (optional) The bundle of the entity. If no bundle is provided, all the + * available bundles are checked. + * @param boolean $skip_handler + * (optional) Specifies whether the availablity of a field translation handler + * should affect the returned value. By default the check is performed. + * + * @returns + * TRUE if the specified bundle is translatable. If no bundle is provided + * returns TRUE if at least one of the entity bundles is translatable. + */ +function translation_entity_enabled($entity_type, $bundle = NULL, $skip_handler = FALSE) { + $enabled = FALSE; + $bundles = !empty($bundle) ? array($bundle) : entity_get_bundles($entity_type); + + foreach ($bundles as $bundle) { + if (translation_entity_get_config($entity_type, $bundle, 'enabled')) { + $enabled = TRUE; + break; + } + } + + return $enabled && ($skip_handler || field_has_translation_handler($entity_type, 'translation_entity')); +} + +/** + * Entity translation controller factory. + * + * @param string $entity_type + * The type of the entity being translated. + * + * @return \Drupal\translation_entity\EntityTranslationControllerInterface + * An instance of the entity translation controller interface. + */ +function translation_entity_controller($entity_type) { + $entity_info = entity_get_info($entity_type); + // @todo Throw an exception if the key is missing. + return new $entity_info['translation_controller_class']($entity_type, $entity_info); +} + +/** + * Returns the entity form controller for the given form. + * + * @param array $form_state + * The form state array holding the entity form controller. + * + * @return \Drupal\Core\Entity\EntityFormControllerInterface; + * An instance of the entity translation form interface or FALSE if not an + * entity form. + */ +function translation_entity_form_controller(array $form_state) { + return isset($form_state['controller']) && $form_state['controller'] instanceof EntityFormControllerInterface ? $form_state['controller'] : FALSE; +} + +/** + * Checks whether an entity translation is accessible. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be accessed. + * @param string $langcode + * The language of the translation to be accessed. + * + * @return + * TRUE if the current user is allowed to view the translation. + */ +function translation_entity_access(EntityInterface $entity, $langcode) { + return translation_entity_controller($entity->entityType())->getTranslationAccess($entity, $langcode) ; +} + +/** + * Implements hook_permission(). + */ +function translation_entity_permission() { + $permission = array( + 'edit original values' => array( + 'title' => t('Edit original values'), + 'description' => t('Access the entity form in the original language.'), + ), + 'toggle field translatability' => array( + 'title' => t('Toggle field translatability'), + 'description' => t('Toggle translatability of fields performing a bulk update.'), + ), + 'translate any entity' => array( + 'title' => t('Translate any entity'), + 'description' => t('Translate field content for any fieldable entity.'), + ), + ); + + foreach (entity_get_info() as $entity_type => $info) { + if (translation_entity_enabled($entity_type)) { + $label = !empty($info['label']) ? t($info['label']) : $entity_type; + $permission["translate $entity_type entities"] = array( + 'title' => t('Translate entities of type @type', array('@type' => $label)), + 'description' => t('Translate field content for entities of type @type.', array('@type' => $label)), + ); + } + } + + return $permission; +} + +/** + * Implements hook_form_alter(). + */ +function translation_entity_form_alter(array &$form, array &$form_state) { + if (($form_controller = translation_entity_form_controller($form_state)) && ($entity = $form_controller->getEntity($form_state)) && !$entity->isNew()) { + $controller = translation_entity_controller($entity->entityType()); + $controller->entityFormAlter($form, $form_state, $entity); + + // @todo Move the following lines to the code generating the property form + // elements once we have an official #multilingual FAPI key. + $translations = $entity->getTranslationLanguages(); + $form_langcode = $form_controller->getFormLangcode($form_state); + + // Handle fields shared between translations when there is at least one + // translation available or a new one is being created. + if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) { + if ($entity instanceof EntityNG) { + foreach ($entity->getPropertyDefinitions() as $property_name => $definition) { + if (isset($form[$property_name])) { + $form[$property_name]['#multilingual'] = !empty($definition['translatable']); + } + } + } + else { + foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + $form[$field_name]['#multilingual'] = !empty($field['translatable']); + } + } + } + + } +} + +/** + * Implements hook_entity_load(). + */ +function translation_entity_entity_load(array $entities, $entity_type) { + $enabled_entities = array(); + + if (translation_entity_enabled($entity_type)) { + foreach ($entities as $entity) { + if (translation_entity_enabled($entity_type, $entity->bundle())) { + $enabled_entities[$entity->id()] = $entity; + } + } + } + + if (!empty($enabled_entities)) { + translation_entity_load_translation_data($enabled_entities, $entity_type); + } +} + +/** + * Loads translation data into the given entities. + * + * @param array $entities + * The entities keyed by entity ID. + * @param string $entity_type + * The type of the entities. + */ +function translation_entity_load_translation_data(array $entities, $entity_type) { + $result = db_select('translation_entity', 'te') + ->fields('te', array()) + ->condition('te.entity_type', $entity_type) + ->condition('te.entity_id', array_keys($entities)) + ->execute(); + + foreach ($result as $record) { + $entity = $entities[$record->entity_id]; + // @todo Declare these as entity (translation?) properties. + $entity->source[$record->langcode] = $record->source; + // @todo Rename to 'translate' when the column is removed from the node + // schema. + $entity->retranslate[$record->langcode] = (boolean) $record->translate; + } +} + +/** + * Implements hook_entity_insert(). + */ +function translation_entity_entity_insert(EntityInterface $entity) { + $entity_type = $entity->entityType(); + $id = $entity->id(); + $query = db_insert('translation_entity') + ->fields(array('entity_type', 'entity_id', 'langcode', 'source', 'translate')); + + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + // @todo Declare these as entity (translation?) properties. + $source = (isset($entity->source[$langcode]) ? $entity->source[$langcode] : NULL) . ''; + $retranslate = intval(!empty($entity->retranslate[$langcode])); + $query->values(array($entity_type, $id, $langcode, $source, $retranslate)); + } + + $query->execute(); +} + +/** + * Implements hook_entity_delete(). + */ +function translation_entity_entity_delete(EntityInterface $entity) { + db_delete('translation_entity') + ->condition('entity_type', $entity->entityType()) + ->condition('entity_id', $entity->id()) + ->execute(); +} + +/** + * Implements hook_entity_update(). + */ +function translation_entity_entity_update(EntityInterface $entity) { + // Delete and create to ensure no stale value remains behind. + translation_entity_entity_delete($entity); + translation_entity_entity_insert($entity); +} + +/** + * Implements hook_field_extra_fields(). + */ +function translation_entity_field_extra_fields() { + $extra = array(); + + foreach (entity_get_info() as $entity_type => $info) { + foreach (entity_get_bundles($entity_type) as $bundle) { + if (translation_entity_enabled($entity_type, $bundle)) { + $extra[$entity_type][$bundle]['form']['translation'] = array( + 'label' => t('Translation'), + 'description' => t('Translation settings'), + 'weight' => 10, + ); + } + } + } + + return $extra; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function translation_entity_form_field_ui_field_edit_form_alter(array &$form, array &$form_state, $form_id) { + $field = $form['#field']; + $field_name = $field['field_name']; + $translatable = $field['translatable']; + $label = t('Field translation'); + $title = t('Users may translate this field.'); + + $form['field']['#collapsed'] = $translatable; + + if (field_has_data($field)) { + $path = "admin/config/regional/translation_entity/translatable/$field_name"; + $status = $translatable ? $title : t('This field has data in existing content.'); + $link_title = !$translatable ? t('Enable translation') : t('Disable translation'); + + $form['field']['translatable'] = array( + '#prefix' => '
', + '#suffix' => '
', + 'message' => array( + '#markup' => $status . ' ', + ), + 'link' => array( + '#type' => 'link', + '#title' => $link_title, + '#href' => $path, + '#options' => array('query' => drupal_get_destination()), + '#access' => user_access('toggle field translatability'), + ), + ); + } + else { + $form['field']['translatable'] = array( + '#prefix' => '', + '#type' => 'checkbox', + '#title' => $title, + '#default_value' => $translatable, + ); + } +} + +/** + * Implements hook_element_info_alter(). + */ +function translation_entity_element_info_alter(&$type) { + if (isset($type['language_configuration'])) { + $type['language_configuration']['#process'][] = 'translation_entity_language_configuration_element_process'; + } +} +/** + * Returns a widget to enable entity translation per entity bundle. + * + * Backward compatibility layer to support entities not using the language + * configuration form element. + * + * @todo Remove once all core entities have language configuration. + * + * @param string $entity_type + * The type of the entity being configured for translation. + * @param string $bundle + * The bundle of the entity being configured for translation. + * @param array $form + * The configuration form array. + * @param array $form_state + * The configuration form state array. + */ +function translation_entity_enable_widget($entity_type, $bundle, array &$form, array &$form_state) { + $key = $form_state['translation_entity']['key']; + if (!isset($form_state['language'][$key])) { + $form_state['language'][$key] = array(); + } + $form_state['language'][$key] += array('entity_type' => $entity_type, 'bundle' => $bundle); + $element = translation_entity_language_configuration_element_process(array('#name' => $key), $form_state, $form); + unset($element['translation_entity']['#element_validate']); + return $element; +} + +/** + * Process callback: Expands the language_configuration form element. + * + * @param array $element + * Form API element. + * + * @return + * Processed language configuration element. + */ +function translation_entity_language_configuration_element_process(array $element, array &$form_state, array &$form) { + $form_state['translation_entity']['key'] = $element['#name']; + $context = $form_state['language'][$element['#name']]; + + $element['translation_entity'] = array( + '#type' => 'checkbox', + '#title' => t('Enable translation'), + '#default_value' => translation_entity_enabled($context['entity_type'], $context['bundle']), + '#element_validate' => array('translation_entity_language_configuration_element_validate'), + '#prefix' => '', + ); + + $form['#submit'][] = 'translation_entity_language_configuration_element_submit'; + + return $element; +} + +/** + * Form validation handler for element added with translation_entity_language_configuration_element_process(). + * + * Checks whether translation can be enabled: if language is set to one of the + * special languages and language selector is not hidden, translation cannot be + * enabled. + * + * @see translation_entity_language_configuration_element_submit() + */ +function translation_entity_language_configuration_element_validate($element, array &$form_state, array $form) { + $key = $form_state['translation_entity']['key']; + $values = $form_state['values'][$key]; + if (language_is_locked($values['langcode']) && $values['language_hidden'] && $values['translation_entity']) { + foreach (language_list(LANGUAGE_LOCKED) as $language) { + $locked_languages[] = $language->name; + } + // @todo Set the correct form element name as soon as the element parents + // are correctly set. We should be using NestedArray::getValue() but for + // now we cannot. + form_set_error('', t('Translation is not supported if language is always one of: @locked_languages', array('@locked_languages' => implode(', ', $locked_languages)))); + } +} + +/** + * Form submission handler for element added with translation_entity_language_configuration_element_process(). + * + * Stores the entity translation settings. + * + * @see translation_entity_language_configuration_element_validate() + */ +function translation_entity_language_configuration_element_submit(array $form, array &$form_state) { + $key = $form_state['translation_entity']['key']; + $context = $form_state['language'][$key]; + $enabled = $form_state['values'][$key]['translation_entity']; + + if (translation_entity_enabled($context['entity_type'], $context['bundle']) != $enabled) { + translation_entity_set_config($context['entity_type'], $context['bundle'], 'enabled', $enabled); + entity_info_cache_clear(); + menu_router_rebuild(); + } +} diff --git a/core/modules/translation_entity/translation_entity.pages.inc b/core/modules/translation_entity/translation_entity.pages.inc new file mode 100644 index 0000000000000000000000000000000000000000..be06e6aa7250fde7339e33e1ccd841fe81eda73d --- /dev/null +++ b/core/modules/translation_entity/translation_entity.pages.inc @@ -0,0 +1,263 @@ +entityType()); + $languages = language_list(); + $original = $entity->language()->langcode; + $translations = $entity->getTranslationLanguages(); + $field_ui = module_exists('field_ui'); + + $path = $controller->getViewPath($entity); + $base_path = $controller->getBasePath($entity); + $edit_path = $controller->getEditPath($entity); + + $header = array(t('Language'), t('Translation'), t('Source language'), t('Status'), t('Operations')); + $rows = array(); + + if (language_multilingual()) { + // If we have a view path defined for the current entity get the switch + // links based on it. + if ($path) { + $links = _translation_entity_get_switch_links($path); + } + + // Determine whether the current entity is translatable. + $translatable = FALSE; + foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + if ($field['translatable']) { + $translatable = TRUE; + break; + } + } + + foreach ($languages as $language) { + $language_name = $language->name; + $langcode = $language->langcode; + $add_path = $base_path . '/translations/add/' . $original . '/' . $langcode; + $delete_path = $base_path . '/translations/delete/' . $langcode; + + if ($base_path) { + $add_links = _translation_entity_get_switch_links($add_path); + $edit_links = _translation_entity_get_switch_links($edit_path); + $delete_links = _translation_entity_get_switch_links($delete_path); + } + + $operations = array( + 'data' => array( + '#type' => 'operations', + '#links' => array(), + ), + ); + $links = &$operations['data']['#links']; + + if (isset($translations[$langcode])) { + // Existing translation in the translation set: display status. + $source = isset($entity->source[$langcode]) ? $entity->source[$langcode] : ''; + $is_original = $langcode == $original; + $translation = $translations[$langcode]; + $label = $entity->label($langcode); + $link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $language); + $row_title = l($label, $link['href'], $link); + + if (empty($link['href'])) { + $row_title = $is_original ? $label : t('n/a'); + } + + if ($edit_path && $controller->getAccess($entity, 'update') && $controller->getTranslationAccess($entity, $langcode)) { + $links['edit'] = isset($edit_links->links[$langcode]['href']) ? $edit_links->links[$langcode] : array('href' => $edit_path, 'language' => $language); + $links['edit']['title'] = t('edit'); + } + + // @todo Consider supporting the ability to track translation publishing + // status independently from entity status, as it may not exist. + $translation = $entity->getTranslation($langcode, FALSE); + $status = !isset($translation->status) || $translation->status ? t('Published') : t('Not published'); + // @todo Add a theming function here. + $status = '' . $status . '' . (!empty($entity->retranslate[$langcode]) ? ' ' . t('outdated') . '' : ''); + + if ($is_original) { + $language_name = t('@language_name', array('@language_name' => $language_name)); + $source_name = t('n/a'); + } + else { + $source_name = isset($languages[$source]) ? $languages[$source]->name : t('n/a'); + $links['delete'] = isset($delete_links->links[$langcode]['href']) ? $delete_links->links[$langcode] : array('href' => $delete_links, 'language' => $language); + $links['delete']['title'] = t('delete'); + } + } + else { + // No such translation in the set yet: help user to create it. + $row_title = $source_name = t('n/a'); + $source = $entity->language()->langcode; + + if ($source != $langcode && $controller->getAccess($entity, 'update')) { + if ($translatable) { + $links['add'] = isset($add_links->links[$langcode]['href']) ? $add_links->links[$langcode] : array('href' => $add_path, 'language' => $language); + $links['add']['title'] = t('add'); + } + elseif ($field_ui) { + $entity_path = _field_ui_bundle_admin_path($entity->entityType(), $entity->bundle()); + // Link directly to the fields tab to make it easier to find the + // setting to enable translation on fields. + $path = $entity_path . '/fields'; + $links['nofields'] = array('title' => t('no translatable fields'), 'href' => $path, 'language' => $language); + } + } + + $status = t('Not translated'); + } + + $rows[] = array($language_name, $row_title, $source_name, $status, $operations); + } + } + + drupal_set_title(t('Translations of %label', array('%label' => $entity->label())), PASS_THROUGH); + + // Add metadata to the build render array to let other modules know about + // which entity this is. + $build['#entity'] = $entity; + + $build['translation_entity_overview'] = array( + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + ); + + return $build; +} + +/** + * Returns the localized links for the given path. + * + * @param string $path + * The path for which language switch links should be provided. + * + * @returns + * A renderable array of language switch links. + */ +function _translation_entity_get_switch_links($path) { + $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path); + if (empty($links)) { + // If content language is set up to fall back to the interface language, + // then there will be no switch links for LANGUAGE_TYPE_CONTENT, ergo we + // also need to use interface switch links. + $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_INTERFACE, $path); + } + return $links; +} + +/** + * Page callback for the translation addition page. + * + * @param EntityInterface $entity + * The entity being translated. + * @param Language $source + * (optional) The language of the values being translated. Defaults to the + * entity language. + * @param Language $target + * (optional) The language of the translated values. Defaults to the current + * content language. + * + * @return array + * A processed form array ready to be rendered. + */ +function translation_entity_add_page(EntityInterface $entity, Language $source = NULL, Language $target = NULL) { + $source = !empty($source) ? $source : $entity->language(); + $target = !empty($target) ? $target : language(LANGUAGE_TYPE_CONTENT); + // @todo Exploit the upcoming hook_entity_prepare() when available. + translation_entity_prepare_translation($entity, $source, $target); + $info = $entity->entityInfo(); + $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default'; + $form_state = entity_form_state_defaults($entity, $operation, $target->langcode); + $form_state['translation_entity']['source'] = $source; + $form_state['translation_entity']['target'] = $target; + $form_id = entity_form_id($entity); + return drupal_build_form($form_id, $form_state); +} + +/** + * Populates target values with the source values. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entitiy being translated. + * @param \Drupal\Core\Language\Language $source + * The language to be used as source. + * @param \Drupal\Core\Language\Language $target + * The language to be used as target. + */ +function translation_entity_prepare_translation(EntityInterface $entity, Language $source, Language $target) { + // @todo Unify field and property handling. + $instances = field_info_instances($entity->entityType(), $entity->bundle()); + if ($entity instanceof EntityNG) { + $source_translation = $entity->getTranslation($source->langcode); + $target_translation = $entity->getTranslation($target->langcode); + foreach ($target_translation->getPropertyDefinitions() as $property_name => $definition) { + // @todo The value part should not be needed. Remove it as soon as things + // do not break. + $target_translation->$property_name->value = $source_translation->$property_name->value; + } + } + else { + foreach ($instances as $field_name => $instance) { + $field = field_info_field($field_name); + if (!empty($field['translatable'])) { + $value = $entity->get($field_name); + $value[$target->langcode] = isset($value[$source->langcode]) ? $value[$source->langcode] : array(); + $entity->set($field_name, $value); + } + } + } +} + +/** + * Form constructor for the translation deletion confirmation. + */ +function translation_entity_delete_confirm(array $form, array $form_state, EntityInterface $entity, Language $language) { + $langcode = $language->langcode; + $controller = translation_entity_controller($entity->entityType()); + + return confirm_form( + $form, + t('Are you sure you want to delete the @language translation of %label?', array('@language' => $language->name, '%label' => $entity->label())), + $controller->getEditPath($entity), + t('This action cannot be undone.'), + t('Delete'), + t('Cancel') + ); +} + +/** + * Form submission handler for translation_entity_delete_confirm(). + */ +function translation_entity_delete_confirm_submit(array $form, array &$form_state) { + list($entity, $language) = $form_state['build_info']['args']; + $controller = translation_entity_controller($entity->entityType()); + + // Remove the translated values. + $controller->removeTranslation($entity, $language->langcode); + $entity->save(); + + // Remove any existing path alias for the removed translation. + if (module_exists('path')) { + path_delete(array('source' => $controller->getViewPath($entity), 'langcode' => $language->langcode)); + } + + $form_state['redirect'] = $controller->getBasePath($entity) . '/translations'; +} diff --git a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php index 68edca19bd47a1397a01027e19d6cba722a3f4f7..dbedf89125800e748092e410f49f6a252c65df53 100644 --- a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php +++ b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php @@ -24,6 +24,8 @@ * "profile" = "Drupal\user\ProfileFormController", * "register" = "Drupal\user\RegisterFormController" * }, + * default_operation = "profile", + * translation_controller_class = "Drupal\user\ProfileTranslationController", * base_table = "users", * uri_callback = "user_uri", * label_callback = "user_label", diff --git a/core/modules/user/lib/Drupal/user/ProfileTranslationController.php b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php new file mode 100644 index 0000000000000000000000000000000000000000..582644bc4e68208995dc29a6a115d9dc942fca24 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php @@ -0,0 +1,42 @@ +getSourceLangcode($form_state)) { + $entity = translation_entity_form_controller($form_state)->getEntity($form_state); + // We need a redirect here, otherwise we would get an access denied page + // since the current URL would be preserved and we would try to add a + // translation for a language that already has a translation. + $form_state['redirect'] = $this->getViewPath($entity); + } + } +} diff --git a/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php b/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php new file mode 100644 index 0000000000000000000000000000000000000000..ae5528fbbc2dcc9f4cec6187531577dcb47513dd --- /dev/null +++ b/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php @@ -0,0 +1,62 @@ + 'User translation UI', + 'description' => 'Tests the user translation UI.', + 'group' => 'User', + ); + } + + /** + * Overrides \Drupal\simpletest\WebTestBase::setUp(). + */ + function setUp() { + $this->entityType = 'user'; + $this->testLanguageSelector = FALSE; + $this->name = $this->randomName(); + parent::setUp(); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission(). + */ + function getTranslatorPermissions() { + return array('administer users', "translate $this->entityType entities", 'edit original values'); + } + + /** + * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues(). + */ + protected function getNewEntityValues($langcode) { + // User name is not translatable hence we use a fixed value. + return array('name' => $this->name) + parent::getNewEntityValues($langcode); + } + +} diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc index 095a6e266f16a87e5af11e02c39f440d52b67126..f4363cf0d88124f8f76af8ad9cd79540282c8ac4 100644 --- a/core/modules/user/user.admin.inc +++ b/core/modules/user/user.admin.inc @@ -307,6 +307,17 @@ function user_admin_settings($form, &$form_state) { '#description' => t('This role will be automatically assigned new permissions whenever a module is enabled. Changing this setting will not affect existing permissions.'), ); + // @todo Remove this check once language settings are generalized. + if (module_exists('translation_entity')) { + $form['language'] = array( + '#type' => 'fieldset', + '#title' => t('Language settings'), + '#tree' => TRUE, + ); + $form_state['translation_entity']['key'] = 'language'; + $form['language'] += translation_entity_enable_widget('user', 'user', $form, $form_state); + } + // User registration settings. $form['registration_cancellation'] = array( '#type' => 'fieldset', diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 700ea7ae9652bf6b3299b7e051a11b05d2f357fa..69118534de183852a80e6326f9384ec048dce463 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1401,6 +1401,8 @@ function user_admin_paths() { 'user/*/cancel' => TRUE, 'user/*/edit' => TRUE, 'user/*/edit/*' => TRUE, + 'user/*/translations' => TRUE, + 'user/*/translations/*' => TRUE, ); return $paths; }