diff --git a/core/includes/entity.inc b/core/includes/entity.inc index 75c3386..d692c9b 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -523,7 +523,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 236fc1c..b3e436c 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -248,36 +248,41 @@ 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(). + * is deprecated by 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 c4ebe28..33aef2d 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormController.php +++ b/core/lib/Drupal/Core/Entity/EntityFormController.php @@ -175,6 +175,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; @@ -209,7 +210,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']; @@ -231,6 +232,49 @@ public function getFormLangcode(array $form_state) { } /** + * Implements EntityFormControllerInterface::isDefaultFormLangcode(). + */ + public function isDefaultFormLangcode($form_state) { + return $this->getFormLangcode($form_state) == $this->getEntity($form_state)->language()->langcode; + } + + /** + * Handle possible entity language changes. + */ + 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(). */ public function buildEntity(array $form, array &$form_state) { diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php index 72c97f1..b550099 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php @@ -52,6 +52,11 @@ public function build(array $form, array &$form_state, EntityInterface $entity); public function getFormLangcode(array $form_state); /** + * Returns TRUE if the enity form language is matches the entity one. + */ + public function isDefaultFormLangcode($form_state); + + /** * Returns the operation identifying the form controller. * * @return string diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php index d06614d..812a192 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/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 1dc938a..8a2e050 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -269,21 +269,13 @@ 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. * * @param bool $enabled diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 27ff7c3..b99a839 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -150,6 +150,12 @@ function comment_entity_info() { ); } + if (module_exists('translation_entity')) { + $return['comment']['translation']['translation_entity'] = array( + 'class' => 'Drupal\comment\CommentTranslationController', + ); + } + return $return; } @@ -1250,10 +1256,24 @@ function comment_form_node_type_form_alter(&$form, $form_state) { DRUPAL_REQUIRED => t('Required'), ), ); + if (module_exists('translation_entity')) { + $form['comment'] += translation_entity_enable_widget($form_state, 'comment_comment_node', $form['#node_type']->type, FALSE); + array_unshift($form['#submit'], 'comment_translation_entity_enable_submit'); + } } } /** + * Checks if translation can be enabled. + */ +function comment_translation_entity_enable_submit($form, &$form_state) { + $comment_form_state = $form_state; + $comment_form_state['translation_entity']['variable_name'] = 'translation_entity_enabled_comment_comment_node_' . $form['#node_type']->type; + $comment_form_state['translation_entity']['element_name'] = 'translation_entity_enabled_comment_comment_node'; + translation_entity_enable_widget_submit($form, $comment_form_state); +} + +/** * Implements hook_form_BASE_FORM_ID_alter(). */ function comment_form_node_form_alter(&$form, $form_state) { 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 0000000..1bd2e57 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php @@ -0,0 +1,34 @@ + $entity->label())); + } +} diff --git a/core/modules/node/content_types.inc b/core/modules/node/content_types.inc index 7388ee9..20b0b52 100644 --- a/core/modules/node/content_types.inc +++ b/core/modules/node/content_types.inc @@ -223,6 +223,13 @@ function node_type_form($form, &$form_state, $type = NULL) { '#title' => t('Hide language selector'), '#default_value' => variable_get('node_type_language_hidden_' . $type->type, TRUE), ); + + if (module_exists('translation_entity')) { + $widget = translation_entity_enable_widget($form_state, 'node', $form['#node_type']->type, FALSE); + $widget['#element_validate'][] = 'node_type_translation_entity_enable_widget_validate'; + $form['#submit'][] = 'translation_entity_enable_widget_submit'; + $form['language'] += $widget; + } } $form['display'] = array( '#type' => 'fieldset', @@ -279,10 +286,27 @@ function node_type_form($form, &$form_state, $type = NULL) { } } + $form['#submit'][] = 'node_type_form_submit'; + return $form; } /** + * Checks if translation can be enabled. + * + * If language is set to one of the special languages and language selector is + * not hidden, translation cannot be enabled. + */ +function node_type_translation_entity_enable_widget_validate($element, &$form_state, $form) { + if (language_is_locked($form_state['values']['node_type_language_default']) && $form_state['values']['node_type_language_hidden'] && $form_state['values']['translation_entity_enabled_node']) { + foreach (language_list(LANGUAGE_LOCKED) as $language) { + $locked_languages[] = $language->name; + } + form_set_error('node_type_language_translation_entity_enabled', t('Translation is not supported if language is always one of: @locked_languages', array('@locked_languages' => implode(", ", $locked_languages)))); + } +} + +/** * Helper function for teaser length choices. * * @param int $length diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php index b9bf7eb..1b6d1fb 100644 --- a/core/modules/node/lib/Drupal/node/NodeFormController.php +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -316,8 +316,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); @@ -331,36 +329,6 @@ public function submit(array $form, array &$form_state) { } /** - * 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. * * @param $form 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 0000000..da078ac --- /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/node.js b/core/modules/node/node.js index 0899d3c..33bc88a 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 translations as outdated') : Drupal.t('Do not flag translations as outdated'); + } + + return translate; + }); } }; diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 2dd1b57..eeab29b 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -239,6 +239,12 @@ function node_entity_info() { $return['node']['translation']['node'] = TRUE; } + if (module_exists('translation_entity')) { + $return['node']['translation']['translation_entity'] = array( + 'class' => 'Drupal\node\NodeTranslationController', + ); + } + // Search integration is provided by node.module, so search-related // view modes for nodes are defined here and not in search.module. if (module_exists('search')) { @@ -2677,7 +2683,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; } @@ -2697,7 +2703,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/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php index e0b25fb..44294af 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php @@ -24,7 +24,7 @@ class EntityTranslationTest extends WebTestBase { * * @var array */ - public static $modules = array('entity_test', 'locale'); + public static $modules = array('entity_test', 'locale', 'node'); protected $langcodes; @@ -302,4 +302,5 @@ function testMultilingualProperties() { $result = $query->execute(); $this->assertEqual(count($result), 1, 'One entity loaded by name, uid and field value using different language meta conditions.'); } + } 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 46def82..57704d5 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\EntityTest; + + /** * Implements hook_entity_info(). */ @@ -23,11 +26,19 @@ function entity_test_entity_info() { 'id' => 'id', 'uuid' => 'uuid', ), + 'menu base path' => 'entity-test/manage/%entity_test', + 'translation' => array( + 'translation_entity' => array( + 'class' => 'Drupal\entity_test\EntityTestTranslationController', + ), + ), ); + // Optionally specify a translation handler for testing translations. if (variable_get('entity_test_translation')) { $items['entity_test']['translation']['entity_test'] = TRUE; } + return $items; } @@ -85,8 +96,8 @@ function entity_test_add() { /** * Menu callback: displays the 'Edit existing entity_test' form. */ -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/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php index 17e8470..1a19cf4 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php @@ -55,4 +55,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/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 e51f474..a65f06f 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 67e4b9b..41aa9dc 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 @@ -85,6 +85,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); @@ -96,12 +103,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 0000000..e7ca050 --- /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/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php new file mode 100644 index 0000000..a8e6dd7 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php @@ -0,0 +1,38 @@ +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/VocabularyFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php index bdeea4c..c80d598 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php @@ -61,6 +61,10 @@ public function form(array $form, array &$form_state, EntityInterface $vocabular $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid); } + if (module_exists('translation_entity')) { + $form += translation_entity_enable_widget($form_state, 'taxonomy_term', $vocabulary->machine_name); + } + return parent::form($form, $form_state, $vocabulary); } @@ -72,6 +76,9 @@ protected function actions(array $form, array &$form_state) { if (empty($form_state['confirm_delete'])) { $actions = parent::actions($form, $form_state); array_unshift($actions['delete']['#submit'], array($this, 'submit')); + if (module_exists('translation_entity')) { + array_unshift($actions['submit']['#submit'], 'translation_entity_enable_widget_submit'); + } return $actions; } else { @@ -139,6 +146,11 @@ public function save(array $form, array &$form_state) { break; } + $variable_name = 'translation_entity_enabled_taxonomy_term_' . $form_state['values']['machine_name']; + if (isset($form_state['values'][$variable_name])) { + variable_set($variable_name, $form_state['values'][$variable_name]); + } + $form_state['values']['vid'] = $vocabulary->vid; $form_state['vid'] = $vocabulary->vid; } diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index faa04dd..b614947 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -139,6 +139,7 @@ function taxonomy_entity_info() { ), ), ); + foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) { $return['taxonomy_term']['bundles'][$machine_name] = array( 'label' => $vocabulary->name, @@ -150,6 +151,14 @@ function taxonomy_entity_info() { ), ); } + + if (module_exists('translation_entity')) { + $return['taxonomy_term']['menu base path'] = 'taxonomy/term/%taxonomy_term'; + $return['taxonomy_term']['translation']['translation_entity'] = array( + 'class' => 'Drupal\taxonomy\TermTranslationController', + ); + } + $return['taxonomy_vocabulary'] = array( 'label' => t('Taxonomy vocabulary'), 'entity class' => 'Drupal\taxonomy\Vocabulary', 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 0000000..39d3013 --- /dev/null +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php @@ -0,0 +1,314 @@ +entityType = $entity_type; + $this->entityInfo = entity_get_info($entity_type); + } + + /** + * 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); + $no_translations = count($translations) < 2; + + // 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]) && (!$no_translations || $new_translation)) { + drupal_set_title($this->entityFormTitle($entity) . ' [' . $languages[$form_langcode]->name . ']', PASS_THROUGH); + } + + // Display source language selector only if we are creating a new + // translation and there are at least two translations available. + if (!$no_translations && $new_translation) { + $form['source_langcode'] = array( + '#type' => 'fieldset', + '#title' => t('Source language'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + '#weight' => -100, + '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 && count($translations) > 1) { + $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']['#disabled'] = TRUE; + } + + // 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 || count($translations) > 1) { + $form['translation'] = array( + '#type' => 'fieldset', + '#title' => t('Translation'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + '#weight' => 10, + '#access' => $this->getTranslationAccess($entity, $form_langcode), + ); + + $translate = !$new_translation && $entity->retranslate[$form_langcode]; + if (!$translate) { + $form['translation']['retranslate'] = array( + '#type' => 'checkbox', + '#title' => t('Flag translations as outdated'), + '#default_value' => FALSE, + '#description' => t('If you made a significant change, which means translations should be updated, you can flag all translations of this post as outdated. This will not change any other property of those posts, 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 because the source post has changed. Uncheck when the translation is up to date again.'), + ); + } + } + + // 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'); + } + } + + /** + * Entity builder method. + */ + public function entityFormEntityBuild($entity_type, $entity, $form, &$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); + } + } + + /** + * Submit handler for the source language change. + */ + public function entityFormSourceChange($form, &$form_state) { + $form_controller = translation_entity_form_controller($form_state); + $entity = $form_controller->getEntity($form_state); + $langcode = $form_state['values']['source_langcode']['source']; + $path = $this->getBasePath($entity) . '/translations/add/' . $langcode; + $form_state['redirect'] = array('path' => $path); + $languages = language_list(); + drupal_set_message(t('Source language set to: %language', array('%language' => t($languages[$langcode]->name)))); + } + + /** + * Submit handler for the 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) { + $info = $entity->entityInfo(); + drupal_set_message(t('This will delete all the @entity_type translations.', array('@entity_type' => drupal_strtolower($info['label']))), 'warning'); + } + } + + /** + * Submit handler for the 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. + */ + 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 + * 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 0000000..6c7ebc9 --- /dev/null +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php @@ -0,0 +1,96 @@ + 'Translation UI', + 'description' => 'Tests the basic entity translation UI.', + 'group' => 'Entity Translation UI', + ); + } + + function setUp() { + parent::setUp(); + + $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); + $this->drupalLogin($admin_user); + + $languages = array('it' => 'Italian', 'fr' => 'French'); + $this->langcodes = array_keys($languages); + array_unshift($this->langcodes, language_default()->langcode); + + // Add predefined language. + foreach ($languages as $langcode => $name) { + $edit = array('predefined_langcode' => $langcode); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + $this->assertText($name, 'Language added successfully.'); + } + + // @todo This should not be need. + // Enable URL language detection for content language. + $edit = array('language_content[enabled][language-url]' => TRUE); + $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings')); + + // Enable translation for the entity_test entity type and ensure the change + // is picked up. + variable_set('translation_entity_enabled_entity_test_entity_test', TRUE); + drupal_static_reset(); + entity_info_cache_clear(); + menu_router_rebuild(); + + $translator = $this->drupalCreateUser(array('administer entity_test content', 'translate entity_test entities', 'edit original values')); + $this->drupalLogin($translator); + } + + /** + * Tests the basic translation UI. + */ + function testTranslationUI() { + // Make the test field translatable. + $field = field_info_field('field_test_text'); + $field['translatable'] = TRUE; + field_update_field($field); + + // Create a new test entity with original values in the default language. + $default_langcode = language_default()->langcode; + + $values[$default_langcode] = array( + 'langcode' => $default_langcode, + 'name' => $this->randomName(8), + 'user_id' => mt_rand(0, 128), + 'field_test_text' => $this->randomName(16), + ); + + $this->drupalPost('entity-test/add', $this->getEditValues($values, $default_langcode, TRUE), t('Save')); + $entity = $this->loadEntityByName($values[$default_langcode]['name']); + $this->assertTrue($entity, t('Entity found in the database.')); + + $translation = $entity->getTranslation($default_langcode); + foreach ($values[$default_langcode] as $property => $value) { + if ($property != 'langcode') { + // @todo Remove this workaround when field default language is correctly + // handled. + $stored_value = $property != 'field_test_text' ? $translation->get($property)->value : $entity->values['field_test_text'][$default_langcode][0]['value']; + $message = format_string('@property correctly stored in the default language.', array('@property' => $property)); + $this->assertEqual($stored_value, $value, $message); + } + } + + // Add an entity translation. + $langcode = 'it'; + $values[$langcode] = array( + 'name' => $this->randomName(8), + 'user_id' => mt_rand(0, 128), + 'field_test_text' => $this->randomName(16), + ); + + $this->drupalPost($langcode . '/entity-test/manage/' . $entity->id() . '/translations/add/' . $default_langcode, $this->getEditValues($values, $langcode), t('Save')); + $this->assertFieldByXPath('//select[@id="edit-langcode" and @disabled]', $default_langcode, 'Language selector correclty disabled on translations.'); + $entity = entity_test_load($entity->id(), TRUE); + + // Switch the source language. + $langcode = 'fr'; + $source_langcode = 'it'; + $edit = array('source_langcode[source]' => $source_langcode); + $this->drupalPost($langcode . '/entity-test/manage/' . $entity->id() . '/translations/add/' . $default_langcode, $edit, t('Change')); + $this->assertFieldByXPath('//input[@name="field_test_text[fr][0][value]"]', $values[$source_langcode]['field_test_text'], 'Source language correctly switched.'); + + // Add another translation and mark the other ones as outdated. + $values[$langcode] = array( + 'name' => $this->randomName(8), + 'user_id' => mt_rand(0, 128), + 'field_test_text' => $this->randomName(16), + ); + $edit = $this->getEditValues($values, $langcode) + array('translation[retranslate]' => TRUE); + $this->drupalPost(NULL, $edit, t('Save')); + $entity = entity_test_load($entity->id(), TRUE); + + // Check that the entered values have been correctly stored. + foreach ($values as $langcode => $property_values) { + $translation = $entity->getTranslation($langcode); + foreach ($property_values as $property => $value) { + if ($property != 'langcode') { + $stored_value = $translation->get($property); + // @todo Remove this workaround when field default language is correctly + // handled. + $stored_value = $property != 'field_test_text' ? $translation->get($property)->value : $entity->values['field_test_text'][$langcode][0]['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 . '/' : ''; + $this->drupalGet($prefix . 'entity-test/manage/' . $entity->id() . '/edit'); + 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(NULL, $edit, t('Save')); + $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.'); + $entity = entity_test_load($entity->id(), TRUE); + $this->assertFalse($entity->retranslate[$enabled_langcode], 'The "outdated" status has been correctly stored.'); + } + } + + // Confirm and delete a translation. + $this->drupalPost(NULL, array(), t('Delete translation')); + $this->drupalPost(NULL, array(), t('Delete')); + $entity = entity_test_load($entity->id(), TRUE); + $translations = $entity->getTranslationLanguages(); + $this->assertTrue(count($translations) == 2 && empty($translations[$enabled_langcode]), 'Translation successfully deleted.'); + } + + /** + * 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; + $edit["field_test_text[$langcode][0][value]"] = $edit['field_test_text']; + unset($edit['field_test_text']); + return $edit; + } + + /** + * Loads a test entity by name. + * + * @return Drupal\entity_test\EntityTest + * A test entity matching the given name. + */ + protected function loadEntityByName($name) { + return current(entity_load_multiple_by_properties('entity_test', array('name' => $name))); + } +} diff --git a/core/modules/translation_entity/tests/modules/translation_entity_test/translation_entity_test.info b/core/modules/translation_entity/tests/modules/translation_entity_test/translation_entity_test.info new file mode 100644 index 0000000..25026b2 --- /dev/null +++ b/core/modules/translation_entity/tests/modules/translation_entity_test/translation_entity_test.info @@ -0,0 +1,8 @@ +name = Entity Translation test module +description = Implements hooks to test the Entity Translaion functionality. +package = Testing +version = VERSION +core = 8.x +dependencies[] = entity_test +dependencies[] = translation_entity +hidden = TRUE diff --git a/core/modules/translation_entity/tests/modules/translation_entity_test/translation_entity_test.module b/core/modules/translation_entity/tests/modules/translation_entity_test/translation_entity_test.module new file mode 100644 index 0000000..240954b --- /dev/null +++ b/core/modules/translation_entity/tests/modules/translation_entity_test/translation_entity_test.module @@ -0,0 +1,6 @@ + $field_name); + + $warning = t('By submitting this form you will trigger a batch operation.'); + 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); +} + +/** + * Submit handler for the field settings 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_NONE] + * 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($form, $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' => $field_name, '!translatable' => $translatable ? t('untranslatable') : t('translatable')); + drupal_set_message(t('The field %field_name is already !translatable. No change was performed.', $t_args), '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); +} + +/* + * Toggle translatability of the given field. + * + * This is called from a batch operation, but should only run once per field. + */ +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 operation. Convert field data to or from LANGUAGE_NOT_SPECIFIED. + */ +function translation_entity_translatable_batch($translatable, $field_name, &$context) { + if (empty($context['sandbox'])) { + $context['sandbox']['progress'] = 0; + + // How many entities will need processing? + $query = new EntityFieldQuery(); + $count = $query + ->fieldCondition($field_name) + ->count() + ->execute(); + + if (intval($count) === 0) { + // Nothing to do. + $context['finished'] = 1; + return; + } + $context['sandbox']['max'] = $count; + } + + // Number of entities to be processed for each step. + $limit = variable_get('translation_entity_translatable_batch_limit', 10); + + $offset = $context['sandbox']['progress']; + $query = new EntityFieldQuery(); + $result = $query + ->fieldCondition($field_name) + ->entityOrderBy('entity_id') + ->range($offset, $limit) + ->execute(); + + foreach ($result as $entity_type => $entities) { + foreach (entity_load_multiple($entity_type, array_keys($entities)) as $id => $entity) { + $context['sandbox']['progress']++; + $langcode = $entity->language()->langcode; + + // Skip process for language neutral entities. + if ($langcode == LANGUAGE_NOT_SPECIFIED) { + continue; + } + + // We need a two-steps 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_NONE 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_NONE 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, $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); + } +} + +/** + * Check the exit status of the batch operation. + */ +function translation_entity_translatable_batch_done($success, $results, $operations) { + if ($success) { + drupal_set_message(t("Data successfully processed.")); + } + 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.")); + } +} + diff --git a/core/modules/translation_entity/translation_entity.info b/core/modules/translation_entity/translation_entity.info new file mode 100644 index 0000000..4a28def --- /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 0000000..c86a5f5 --- /dev/null +++ b/core/modules/translation_entity/translation_entity.install @@ -0,0 +1,52 @@ + '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; +} diff --git a/core/modules/translation_entity/translation_entity.module b/core/modules/translation_entity/translation_entity.module new file mode 100644 index 0000000..cfe928b --- /dev/null +++ b/core/modules/translation_entity/translation_entity.module @@ -0,0 +1,486 @@ + $info) { + if (!isset($entity_info[$entity_type]['translation']['translation_entity'])) { + $entity_info[$entity_type]['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. + $entity_info[$entity_type]['translation']['translation_entity'] += array('class' => 'Drupal\translation_entity\EntityTranslationController'); + + if (translation_entity_enabled($entity_type, NULL, TRUE)) { + // If no menu base path is provided we default to the common "node/%node" + // pattern. + if (!isset($entity_info[$entity_type]['menu base path'])) { + $path = "$entity_type/%$entity_type"; + $entity_info[$entity_type]['menu base path'] = $path; + } + + $path = $entity_info[$entity_type]['menu base path']; + + $entity_info[$entity_type] += array( + 'menu view path' => $path, + 'menu edit path' => "$path/edit", + 'menu path wildcard' => "%$entity_type", + ); + + $entity_position = count(explode('/', $path)) - 1; + $entity_info[$entity_type]['translation']['translation_entity'] += array( + 'access callback' => 'translation_entity_tab_access', + 'access arguments' => array($entity_position), + 'theme callback' => 'variable_get', + 'theme arguments' => array('admin_theme'), + ); + } + } +} + +/** + * 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')); + $item = array_intersect_key($info['translation']['translation_entity'], $keys) + array('file' => 'translation_entity.pages.inc'); + + $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_position = $entity_position + 3; + $args = array($entity_position, $language_position); + $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(&$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. + */ +function translation_entity_tab_access(EntityInterface $entity) { + $entity_type = $entity->entityType(); + return empty($entity->language()->locked) && language_multilingual() && (user_access('translate any entity') || user_access("translate $entity_type entities")); +} + +/** + * Access callback. + */ +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); +} + +/** + * Helper function. Determines whether the given entity type is translatable. + */ +function translation_entity_enabled($entity_type, $bundle = NULL, $skip_handler = FALSE) { + $enabled = FALSE; + + if (!empty($bundle)) { + $enabled = variable_get("translation_entity_enabled_{$entity_type}_{$bundle}"); + } + else { + $entity_info = entity_get_info($entity_type); + $bundles = isset($entity_info['bundles']) ? $entity_info['bundles'] : array($entity_type => NULL); + foreach ($bundles as $bundle => $info) { + if (variable_get("translation_entity_enabled_{$entity_type}_{$bundle}")) { + $enabled = TRUE; + break; + } + } + } + + return $enabled && ($skip_handler || field_has_translation_handler($entity_type, 'translation_entity')); +} + +/** + * Entity translation controller factory. + * + * @param $entity + * 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. + $class = $entity_info['translation']['translation_entity']['class']; + return new $class($entity_type); +} + +/** + * 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 $entity + * The entity to be accessed. + * @param $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(&$form, &$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); + } +} + +/** + * 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_form_FORM_ID_alter(). + */ +function translation_entity_form_field_ui_field_edit_form_alter(&$form, &$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.'); + + if (field_has_data($field)) { + $path = "admin/config/regional/translation_entity/translatable/$field_name"; + $status = $translatable ? $title : t('This field is shared among the entity translations.'); + $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, + ); + } +} + +/** + * Widget to enable entity translation per entity bundle. + * + * The suffix and bundle parameter are supposed to be provided in a way that the + * resulting variable name is always in the following form: + * 'translation_entity_enabled_ENTITY_TYPE_BUNDLE'. + * + * @param $suffix + * An element name suffix, usually the entity type such as 'node' or 'user'. + * @param $bundle + * The bundle name. + * @param $append_bundle + * (optional) If FALSE the bundle name is not appended to variable name. + * For instance node_type_form() automatically saves the form values with the + * bundle name associated. + * + * @return array + * The widget to enable the translation + */ +function translation_entity_enable_widget(&$form_state, $suffix, $bundle, $append_bundle = TRUE) { + $element_name = 'translation_entity_enabled_' . $suffix; + $variable_name = $element_name . '_' . $bundle; + if ($append_bundle) { + $element_name .= '_' . $bundle; + } + + $form_state['translation_entity']['element_name'] = $element_name; + $form_state['translation_entity']['variable_name'] = $variable_name; + + return array( + $element_name => array( + '#type' => 'checkbox', + '#title' => t('Enable translation'), + '#default_value' => variable_get($variable_name, FALSE), + '#element_validate' => array('translation_entity_enable_widget_validate'), + '#prefix' => '', + ), + ); +} + +/** + * Checks if translation can be enabled. + */ +function translation_entity_enable_widget_validate($element, &$form_state, $form) { + // @todo Add common widget validation when entity language settings will be + // generalizated. See node_type_translation_entity_enable_widget_validate(). +} + +/** + * Checks if translation can be enabled. + */ +function translation_entity_enable_widget_submit($form, &$form_state) { + $variable_name = $form_state['translation_entity']['variable_name']; + $enabled = $form_state['values'][$form_state['translation_entity']['element_name']]; + if (variable_get($variable_name) != $enabled) { + variable_set($variable_name, $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 0000000..050b43d --- /dev/null +++ b/core/modules/translation_entity/translation_entity.pages.inc @@ -0,0 +1,223 @@ +entityType()); + $languages = language_list(); + $original = $entity->language()->langcode; + $translations = $entity->getTranslationLanguages(); + + $path = $controller->getViewPath($entity); + $base_path = $controller->getBasePath($entity); + $edit_path = $controller->getEditPath($entity); + + $header = array(t('Language'), t('Source language'), t('Translation'), 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); + } + + foreach ($languages as $language) { + $options = array(); + $language_name = $language->name; + $langcode = $language->langcode; + $add_path = "$base_path/translations/add/$original"; + + if ($base_path) { + $add_links = _translation_entity_get_switch_links($add_path); + $edit_links = _translation_entity_get_switch_links($edit_path); + } + + 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)) { + $link = isset($edit_links->links[$langcode]['href']) ? $edit_links->links[$langcode] : array('href' => $edit_path, 'language' => $language); + $options[] = l(t('edit'), $link['href'], $link); + } + + // @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 .= !empty($entity->retranslate[$langcode]) ? ' - ' . t('outdated') . '' : ''; + + if ($is_original) { + $language_name = t('@language_name', array('@language_name' => $language_name)); + $source_name = t('(original content)'); + } + else { + $source_name = isset($languages[$source]) ? $languages[$source]->name : t('n/a'); + } + } + 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')) { + $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; + } + } + + $link = isset($add_links->links[$langcode]['href']) ? $add_links->links[$langcode] : array('href' => $add_path, 'language' => $language); + $options[] = $translatable ? l(t('add'), $link['href'], $link) : t('No translatable fields'); + } + $status = t('Not translated'); + } + $rows[] = array($language_name, $source_name, $row_title, $status, implode(" | ", $options)); + } + } + + 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. + */ +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. + */ +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. + */ +function translation_entity_prepare_translation(EntityInterface $entity, Language $source, Language $target) { + // @todo Unify field and propesrty 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) { + if (!isset($instances[$property_name])) { + // @todo Actually retrieving the property value should not be necessary. + $target_translation->$property_name = $source_translation->$property_name->value; + } + else { + $entity->setCompatibilityMode(TRUE); + $value = $entity->$property_name; + $value[$target->langcode] = $value[$source->langcode]; + $entity->$property_name = $value; + $entity->setCompatibilityMode(FALSE); + } + } + } + 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] = $value[$source->langcode]; + $entity->set($field_name, $value); + } + } + } +} + +/** + * Translation deletion confirmation form. + */ +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') + ); +} + +/** + * Submit handler for the translation deletion confirmation. + */ +function translation_entity_delete_confirm_submit($form, &$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/ProfileTranslationController.php b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php new file mode 100644 index 0000000..f10d58e --- /dev/null +++ b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php @@ -0,0 +1,38 @@ +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/user/user.admin.inc b/core/modules/user/user.admin.inc index 180b9f1..00f26a1 100644 --- a/core/modules/user/user.admin.inc +++ b/core/modules/user/user.admin.inc @@ -298,6 +298,16 @@ 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.'), ); + if (module_exists('translation_entity')) { + $form['language'] = array( + '#type' => 'fieldset', + '#title' => t('Language settings'), + ); + + $form['language'] += translation_entity_enable_widget($form_state, 'user', 'user'); + $form['#submit'][] = 'translation_entity_enable_widget_submit'; + } + // User registration settings. $form['registration_cancellation'] = array( '#type' => 'fieldset', @@ -678,6 +688,11 @@ function user_admin_settings_submit($form, &$form_state) { ->set('status_canceled.body', $form_state['values']['user_mail_status_canceled_body']) ->set('status_canceled.subject', $form_state['values']['user_mail_status_canceled_subject']) ->save(); + + // @todo Move this to the config system. + if (isset($form_state['values']['translation_entity_enabled_user_user'])) { + variable_set('translation_entity_enabled_user_user', $form_state['values']['translation_entity_enabled_user_user']); + } } /** diff --git a/core/modules/user/user.module b/core/modules/user/user.module index e42002b..1856a7b 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -142,7 +142,7 @@ function user_theme() { * Implements hook_entity_info(). */ function user_entity_info() { - return array( + $return = array( 'user' => array( 'label' => t('User'), 'controller class' => 'Drupal\user\UserStorageController', @@ -150,6 +150,9 @@ function user_entity_info() { 'profile' => 'Drupal\user\ProfileFormController', 'register' => 'Drupal\user\RegisterFormController', ), + // @todo Remove this once the profile form controller is associated to the + // default operation. + 'default operation' => 'profile', 'base table' => 'users', 'uri callback' => 'user_uri', 'label callback' => 'user_label', @@ -176,6 +179,14 @@ function user_entity_info() { ), ), ); + + if (module_exists('translation_entity')) { + $return['user']['translation']['translation_entity'] = array( + 'class' => 'Drupal\user\ProfileTranslationController', + ); + } + + return $return; } /**