Commit 12993dd6 authored by webchick's avatar webchick

Issue #1188388 by plach, peximo, YesCT | Gábor Hojtsy, fago, webchick, Bojhan,...

Issue #1188388 by plach, peximo, YesCT | Gábor Hojtsy, fago, webchick, Bojhan, podarok, cosmicdreams, Berdir, aspilicious, bforchhammer, penyaskito: Added Entity translation UI in core.
parent eb599cd5
......@@ -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));
}
}
......
......@@ -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;
}
/**
......
......@@ -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().
*/
......
......@@ -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.
*
......
......@@ -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));
}
}
......
......@@ -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.
......
......@@ -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.
*
......
......@@ -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().
*/
......
......@@ -220,8 +220,6 @@ protected function actions(array $form, array &$form_state) {
),
);
$element['#weight'] = $form['comment_body']['#weight'] + 0.01;
return $element;
}
......
<?php
/**
* @file
* Definition of Drupal\comment\CommentTranslationController.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\translation_entity\EntityTranslationController;
/**
* Defines the translation controller class for comments.
*/
class CommentTranslationController extends EntityTranslationController {
/**
* Overrides EntityTranslationController::entityFormTitle().
*/
protected function entityFormTitle(EntityInterface $entity) {
return t('Edit comment @subject', array('@subject' => $entity->label()));
}
}
......@@ -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,
......
<?php
/**
* @file
* Definition of Drupal\comment\Tests\CommentTranslationUITest.
*/
namespace Drupal\comment\Tests;
use Drupal\translation_entity\Tests\EntityTranslationUITest;
/**
* Tests the Comment Translation UI.
*/
class CommentTranslationUITest extends EntityTranslationUITest {
/**
* The subject of the test comment.
*/
protected $subject;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'translation_entity', 'node', 'comment');
public static function getInfo() {
return array(
'name' => '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);
}
}
......@@ -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(
......
......@@ -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.
*
......
<?php
/**
* @file
* Definition of Drupal\node\NodeTranslationController.
*/
namespace Drupal\node;
use Drupal\Core\Entity\EntityInterface;
use Drupal\translation_entity\EntityTranslationController;
/**
* Defines the translation controller class for nodes.
*/
class NodeTranslationController extends EntityTranslationController {
/**
* Overrides EntityTranslationController::getAccess().
*/
public function getAccess(EntityInterface $entity, $op) {
return node_access($op, $entity);
}
/**
* Overrides EntityTranslationController::entityFormAlter().
*/
public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity) {
parent::entityFormAlter($form, $form_state, $entity);
// Move the translation fieldset to a vertical tab.
if (isset($form['translation'])) {
$form['translation'] += array(
'#group' => '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('<em>Edit @type</em> @title', array('@type' => $type_name, '@title' => $entity->label()));
}
}
......@@ -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",
......
<?php
/**
* @file
* Definition of Drupal\node\Tests\NodeTranslationUITest.
*/
namespace Drupal\node\Tests;
use Drupal\translation_entity\Tests\EntityTranslationUITest;
/**
* Tests the Node Translation UI.
*/
class NodeTranslationUITest extends EntityTranslationUITest {
/**
* The title of the test node.
*/
protected $title;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('language', 'translation_entity', 'node');
public static function getInfo() {
return array(
'name' => '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);
}
}
......@@ -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;
});
}
};
......
......@@ -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.
......
......@@ -5,6 +5,9 @@
* Test module for the entity API providing an entity type for testing.
*/