summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwebchick2012-11-04 02:38:49 (GMT)
committerwebchick2012-11-04 02:38:49 (GMT)
commit12993dd636c375ed64ad2aa258a2434ce4d65c51 (patch)
treed3f0b96ed66802495ba1c8b6f21011b44d4ad67f
parenteb599cd5dc635def4036c4632044cdb999cbc8ab (diff)
Issue #1188388 by plach, peximo, YesCT | Gábor Hojtsy, fago, webchick, Bojhan, podarok, cosmicdreams, Berdir, aspilicious, bforchhammer, penyaskito: Added Entity translation UI in core.
-rw-r--r--core/includes/entity.inc17
-rw-r--r--core/lib/Drupal/Core/Entity/Entity.php38
-rw-r--r--core/lib/Drupal/Core/Entity/EntityFormController.php51
-rw-r--r--core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php11
-rw-r--r--core/lib/Drupal/Core/Entity/EntityFormControllerNG.php11
-rw-r--r--core/lib/Drupal/Core/Entity/EntityManager.php14
-rw-r--r--core/lib/Drupal/Core/Entity/EntityNG.php14
-rw-r--r--core/modules/comment/comment.module39
-rw-r--r--core/modules/comment/lib/Drupal/comment/CommentFormController.php2
-rw-r--r--core/modules/comment/lib/Drupal/comment/CommentTranslationController.php26
-rw-r--r--core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php1
-rw-r--r--core/modules/comment/lib/Drupal/comment/Tests/CommentTranslationUITest.php95
-rw-r--r--core/modules/node/content_types.inc1
-rw-r--r--core/modules/node/lib/Drupal/node/NodeFormController.php32
-rw-r--r--core/modules/node/lib/Drupal/node/NodeTranslationController.php50
-rw-r--r--core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php1
-rw-r--r--core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php70
-rw-r--r--core/modules/node/node.js15
-rw-r--r--core/modules/node/node.module6
-rw-r--r--core/modules/system/tests/modules/entity_test/entity_test.module20
-rw-r--r--core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php21
-rw-r--r--core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php17
-rw-r--r--core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php28
-rw-r--r--core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php12
-rw-r--r--core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php4
-rw-r--r--core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php42
-rw-r--r--core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php89
-rw-r--r--core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php6
-rw-r--r--core/modules/taxonomy/taxonomy.module2
-rw-r--r--core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php433
-rw-r--r--core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php190
-rw-r--r--core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTestTranslationUITest.php55
-rw-r--r--core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php317
-rw-r--r--core/modules/translation_entity/translation_entity.admin.inc262
-rw-r--r--core/modules/translation_entity/translation_entity.info6
-rw-r--r--core/modules/translation_entity/translation_entity.install71
-rw-r--r--core/modules/translation_entity/translation_entity.module702
-rw-r--r--core/modules/translation_entity/translation_entity.pages.inc263
-rw-r--r--core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php2
-rw-r--r--core/modules/user/lib/Drupal/user/ProfileTranslationController.php42
-rw-r--r--core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php62
-rw-r--r--core/modules/user/user.admin.inc11
-rw-r--r--core/modules/user/user.module2
43 files changed, 3062 insertions, 91 deletions
diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index fd5a880..8cb009a 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -54,6 +54,21 @@ function entity_info_cache_clear() {
}
/**
+ * 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.
*
* @param string $entity_type
@@ -445,7 +460,7 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st
// Invoke all specified builders for copying form values to entity properties.
if (isset($form['#entity_builders'])) {
foreach ($form['#entity_builders'] as $function) {
- $function($entity_type, $entity, $form, $form_state);
+ call_user_func_array($function, array($entity_type, $entity, &$form, &$form_state));
}
}
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 710792f..c6e041e 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -275,37 +275,43 @@ class Entity implements IteratorAggregate, EntityInterface {
/**
* Returns the languages the entity is translated to.
*
- * @todo: Remove once all entity types implement the entity field API. This
- * is deprecated by
- * TranslatableInterface::getTranslationLanguages().
+ * @todo: Remove once all entity types implement the entity field API.
+ * This is deprecated by
+ * Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages().
*/
public function translations() {
- $languages = array();
+ return $this->getTranslationLanguages(FALSE);
+ }
+
+ /**
+ * Implements TranslatableInterface::getTranslationLanguages().
+ */
+ public function getTranslationLanguages($include_default = TRUE) {
+ // @todo: Replace by EntityNG implementation once all entity types have been
+ // converted to use the entity field API.
+ $default_language = $this->language();
+ $languages = array($default_language->langcode => $default_language);
$entity_info = $this->entityInfo();
- if ($entity_info['fieldable'] && ($default_language = $this->language())) {
+
+ if ($entity_info['fieldable']) {
// Go through translatable properties and determine all languages for
// which translated values are available.
foreach (field_info_instances($this->entityType, $this->bundle()) as $field_name => $instance) {
$field = field_info_field($field_name);
if (field_is_translatable($this->entityType, $field) && isset($this->$field_name)) {
- foreach ($this->$field_name as $langcode => $value) {
+ foreach (array_filter($this->$field_name) as $langcode => $value) {
$languages[$langcode] = TRUE;
}
}
}
- // Remove the default language from the translations.
+ $languages = array_intersect_key(language_list(LANGUAGE_ALL), $languages);
+ }
+
+ if (empty($include_default)) {
unset($languages[$default_language->langcode]);
- $languages = array_intersect_key(language_list(), $languages);
}
- return $languages;
- }
- /**
- * Implements TranslatableInterface::getTranslationLanguages().
- */
- public function getTranslationLanguages($include_default = TRUE) {
- // @todo: Replace by EntityNG implementation once all entity types have been
- // converted to use the entity field API.
+ return $languages;
}
/**
diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php
index f75015f..2e9daac 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormController.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormController.php
@@ -178,6 +178,7 @@ class EntityFormController implements EntityFormControllerInterface {
* 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 @@ class EntityFormController implements EntityFormControllerInterface {
*/
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'];
@@ -234,6 +235,54 @@ class EntityFormController implements EntityFormControllerInterface {
}
/**
+ * 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().
*/
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 26a227d..fecb078 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php
@@ -44,6 +44,17 @@ interface EntityFormControllerInterface {
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.
*
* @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 @@ class EntityFormControllerNG extends EntityFormController {
// without changing existing entity properties that are not being edited by
// this form. Copying field values must be done using field_attach_submit().
$values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $entity->bundle())) : $form_state['values'];
+ $translation = $entity->getTranslation($this->getFormLangcode($form_state), FALSE);
+ $definitions = $translation->getPropertyDefinitions();
foreach ($values_excluding_fields as $key => $value) {
- $entity->$key = $value;
+ if (isset($definitions[$key])) {
+ $translation->$key = $value;
+ }
}
- // Invoke all specified builders for copying form values to entity properties.
+ // Invoke all specified builders for copying form values to entity
+ // properties.
if (isset($form['#entity_builders'])) {
foreach ($form['#entity_builders'] as $function) {
- $function($entity_type, $entity, $form, $form_state);
+ call_user_func_array($function, array($entity_type, $entity, &$form, &$form_state));
}
}
diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php
index b8c38ce..29f0a36 100644
--- a/core/lib/Drupal/Core/Entity/EntityManager.php
+++ b/core/lib/Drupal/Core/Entity/EntityManager.php
@@ -59,6 +59,10 @@ use Drupal\Core\Cache\CacheBackendInterface;
* 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 @@ use Drupal\Core\Cache\CacheBackendInterface;
* by default (e.g. right after the module exposing the view mode is
* enabled), but administrators can later use the Field UI to apply custom
* display settings specific to the view mode.
+ * - menu_base_path: (optional) The base menu router path to which the entity
+ * administration user interface responds. It can be used to generate UI
+ * links and to attach additional router items to the entity UI in a generic
+ * fashion.
+ * - menu_view_path: (optional) The menu router path to be used to view the
+ * entity.
+ * - menu_edit_path: (optional) The menu router path to be used to edit the
+ * entity.
+ * - menu_path_wildcard: (optional) A string identifying the menu loader in the
+ * router path.
*
* The defaults for the plugin definition are provided in
* \Drupal\Core\Entity\EntityManager::defaults.
diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php
index 2fc7764..35ef89d 100644
--- a/core/lib/Drupal/Core/Entity/EntityNG.php
+++ b/core/lib/Drupal/Core/Entity/EntityNG.php
@@ -269,21 +269,13 @@ class EntityNG extends Entity {
$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 c174293..7b6eda4 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -1009,6 +1009,16 @@ function comment_links(Comment $comment, Node $node) {
$links['comment-forbidden']['html'] = TRUE;
}
}
+
+ // Add translations link for translation-enabled comment bundles.
+ if (module_exists('translation_entity') && translation_entity_translate_access($comment)) {
+ $links['comment-translations'] = array(
+ 'title' => t('translations'),
+ 'href' => 'comment/' . $comment->id() . '/translations',
+ 'html' => TRUE,
+ );
+ }
+
return $links;
}
@@ -1106,10 +1116,39 @@ 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().
*/
function comment_form_node_form_alter(&$form, $form_state) {
diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php
index cead0cd..45ae3df 100644
--- a/core/modules/comment/lib/Drupal/comment/CommentFormController.php
+++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php
@@ -220,8 +220,6 @@ class CommentFormController extends EntityFormController {
),
);
- $element['#weight'] = $form['comment_body']['#weight'] + 0.01;
-
return $element;
}
diff --git a/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php b/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php
new file mode 100644
index 0000000..0cfa52b
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php
@@ -0,0 +1,26 @@
+<?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()));
+ }
+
+}
diff --git a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
index 2b00d45..e0bbfd9 100644
--- a/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
+++ b/core/modules/comment/lib/Drupal/comment/Plugin/Core/Entity/Comment.php
@@ -24,6 +24,7 @@ use Drupal\Core\Annotation\Translation;
* form_controller_class = {
* "default" = "Drupal\comment\CommentFormController"
* },
+ * translation_controller_class = "Drupal\comment\CommentTranslationController",
* base_table = "comment",
* uri_callback = "comment_uri",
* fieldable = TRUE,
diff --git a/core/modules/comment/lib/Drupal/comment/Tests/CommentTranslationUITest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentTranslationUITest.php
new file mode 100644
index 0000000..75ca1d8
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentTranslationUITest.php
@@ -0,0 +1,95 @@
+<?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);
+ }
+
+}
diff --git a/core/modules/node/content_types.inc b/core/modules/node/content_types.inc
index a7faa59..a6839ae 100644
--- a/core/modules/node/content_types.inc
+++ b/core/modules/node/content_types.inc
@@ -224,6 +224,7 @@ function node_type_form($form, &$form_state, $type = NULL) {
),
'#default_value' => $language_configuration,
);
+
$form['#submit'][] = 'language_configuration_element_submit';
}
$form['display'] = array(
diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php
index 5add3df..563a00f 100644
--- a/core/modules/node/lib/Drupal/node/NodeFormController.php
+++ b/core/modules/node/lib/Drupal/node/NodeFormController.php
@@ -317,8 +317,6 @@ class NodeFormController extends EntityFormController {
* 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);
@@ -337,36 +335,6 @@ class NodeFormController extends EntityFormController {
}
/**
- * 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 @@
+<?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()));
+ }
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
index f161d81..a9139e9 100644
--- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
@@ -24,6 +24,7 @@ use Drupal\Core\Annotation\Translation;
* form_controller_class = {
* "default" = "Drupal\node\NodeFormController"
* },
+ * translation_controller_class = "Drupal\node\NodeTranslationController",
* base_table = "node",
* revision_table = "node_revision",
* uri_callback = "node_uri",
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php
new file mode 100644
index 0000000..17c7fec
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeTranslationUITest.php
@@ -0,0 +1,70 @@
+<?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);
+ }
+
+}
diff --git a/core/modules/node/node.js b/core/modules/node/node.js
index 0899d3c..e54a4d4 100644
--- a/core/modules/node/node.js
+++ b/core/modules/node/node.js
@@ -42,6 +42,21 @@ Drupal.behaviors.nodeFieldsetSummaries = {
}
return vals.join(', ');
});
+
+ $context.find('fieldset.node-translation-options').drupalSetSummary(function (context) {
+ var translate;
+ var $checkbox = $context.find('.form-item-translation-translate input');
+
+ if ($checkbox.size()) {
+ translate = $checkbox.is(':checked') ? Drupal.t('Needs to be updated') : Drupal.t('Does not need to be updated');
+ }
+ else {
+ $checkbox = $context.find('.form-item-translation-retranslate input');
+ translate = $checkbox.is(':checked') ? Drupal.t('Flag other translations as outdated') : Drupal.t('Do not flag other translations as outdated');
+ }
+
+ return translate;
+ });
}
};
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 5c11424..fc53461 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -264,6 +264,8 @@ function node_admin_paths() {
'node/*/revisions' => TRUE,
'node/*/revisions/*/revert' => TRUE,
'node/*/revisions/*/delete' => TRUE,
+ 'node/*/translations' => TRUE,
+ 'node/*/translations/*' => TRUE,
'node/add' => TRUE,
'node/add/*' => TRUE,
);
@@ -2449,7 +2451,7 @@ function node_update_index() {
$counter = 0;
foreach (node_load_multiple($nids) as $node) {
// Determine when the maximum number of indexable items is reached.
- $counter += 1 + count($node->translations());
+ $counter += count($node->getTranslationLanguages());
if ($counter > $limit) {
break;
}
@@ -2469,7 +2471,7 @@ function _node_index_node(Node $node) {
// results half-life calculation.
variable_set('node_cron_last', $node->changed);
- $languages = array_merge(array(language_load($node->langcode)), $node->translations());
+ $languages = $node->getTranslationLanguages();
foreach ($languages as $language) {
// Render the node.
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 9ee32ec..7844dcb 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -5,6 +5,9 @@
* Test module for the entity API providing an entity type for testing.
*/
+use Drupal\entity_test\Plugin\Core\Entity\EntityTest;
+
+
/**
* Implements hook_entity_info_alter().
*/
@@ -59,6 +62,11 @@ function entity_test_menu() {
/**
* Menu callback: displays the 'Add new entity_test' form.
+ *
+ * @return array
+ * The processed form for a new entity_test.
+ *
+ * @see entity_test_menu()
*/
function entity_test_add() {
drupal_set_title(t('Create an entity_test'));
@@ -68,9 +76,17 @@ function entity_test_add() {
/**
* Menu callback: displays the 'Edit existing entity_test' form.
+ *
+ * @param array $entity
+ * The entity to be edited.
+ *
+ * @return array
+ * The processed form for the edited entity_test.
+ *
+ * @see entity_test_menu()
*/
-function entity_test_edit($entity) {
- drupal_set_title(t('entity_test @id', array('@id' => $entity->id())), PASS_THROUGH);
+function entity_test_edit(EntityTest $entity) {
+ drupal_set_title($entity->label(), PASS_THROUGH);
return entity_get_form($entity);
}
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
index 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 @@ class EntityTestFormController extends EntityFormControllerNG {
'#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 fbb735d..da502b8 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
@@ -84,6 +84,13 @@ class EntityTestStorageController extends DatabaseStorageControllerNG {
protected function postSave(EntityInterface $entity, $update) {
$default_langcode = $entity->language()->langcode;
+ // Delete and insert to handle removed values.
+ db_delete('entity_test_property_data')
+ ->condition('id', $entity->id())
+ ->execute();
+
+ $query = db_insert('entity_test_property_data');
+
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$translation = $entity->getTranslation($langcode);
@@ -95,12 +102,12 @@ class EntityTestStorageController extends DatabaseStorageControllerNG {
'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 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\translation_entity\EntityTranslationController.
+ */
+
+namespace Drupal\entity_test;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\translation_entity\EntityTranslationController;
+
+/**
+ * Test entity translation controller.
+ */
+class EntityTestTranslationController extends EntityTranslationController {
+
+ /**
+ * Overrides EntityTranslationControllerInterface::removeTranslation().
+ */
+ public function removeTranslation(EntityInterface $entity, $langcode) {
+ $translation = $entity->getTranslation($langcode);
+ foreach ($translation->getPropertyDefinitions() as $property_name => $langcode) {
+ $translation->$property_name = array();
+ }
+ }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php
index 240c5c7..3c948bd 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Plugin/Core/Entity/EntityTest.php
@@ -22,13 +22,15 @@ use Drupal\Core\Annotation\Translation;
* form_controller_class = {
* "default" = "Drupal\entity_test\EntityTestFormController"
* },
+ * translation_controller_class = "Drupal\entity_test\EntityTestTranslationController",
* base_table = "entity_test",
* data_table = "entity_test_property_data",
* fieldable = TRUE,
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid"
- * }
+ * },
+ * menu_base_path = "entity-test/manage/%entity_test"
* )
*/
class EntityTest extends EntityNG {
@@ -74,4 +76,12 @@ class EntityTest extends EntityNG {
unset($this->name);
unset($this->user_id);
}
+
+ /**
+ * Overrides Drupal\entity\Entity::label().
+ */
+ public function label($langcode = LANGUAGE_DEFAULT) {
+ return $this->getTranslation($langcode)->name->value;
+ }
+
}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
index 8aedeee..51468af0 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Plugin/Core/Entity/Term.php
@@ -24,6 +24,7 @@ use Drupal\Core\Annotation\Translation;
* form_controller_class = {
* "default" = "Drupal\taxonomy\TermFormController"
* },
+ * translation_controller_class = "Drupal\taxonomy\TermTranslationController",
* base_table = "taxonomy_term_data",
* uri_callback = "taxonomy_term_uri",
* fieldable = TRUE,
@@ -41,7 +42,8 @@ use Drupal\Core\Annotation\Translation;
* "label" = "Taxonomy term page",
* "custom_settings" = FALSE
* }
- * }
+ * },
+ * menu_base_path = "taxonomy/term/%taxonomy_term"
* )
*/
class Term extends Entity implements ContentEntityInterface {
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php
new file mode 100644
index 0000000..2fa4227
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\taxonomy\TermTranslationController.
+ */
+
+namespace Drupal\taxonomy;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\translation_entity\EntityTranslationController;
+
+/**
+ * Defines the translation controller class for terms.
+ */
+class TermTranslationController extends EntityTranslationController {
+
+ /**
+ * Overrides EntityTranslationController::entityFormAlter().
+ */
+ public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity) {
+ parent::entityFormAlter($form, $form_state, $entity);
+ $form['actions']['submit']['#submit'][] = array($this, 'entityFormSave');
+ }
+
+ /**
+ * Form submission handler for TermTranslationController::entityFormAlter().
+ *
+ * This handles the save action.
+ *
+ * @see \Drupal\Core\Entity\EntityFormController::build().
+ */
+ function entityFormSave(array $form, array &$form_state) {
+ if ($this->getSourceLangcode($form_state)) {
+ $entity = translation_entity_form_controller($form_state)->getEntity($form_state);
+ // We need a redirect here, otherwise we would get an access denied page
+ // since the curret URL would be preserved and we would try to add a
+ // translation for a language that already has a translation.
+ $form_state['redirect'] = $this->getEditPath($entity);
+ }
+ }
+}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php
new file mode 100644
index 0000000..907ce87
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTranslationUITest.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\taxonomy\Tests\TermTranslationUITest.
+ */
+
+namespace Drupal\taxonomy\Tests;
+
+use Drupal\translation_entity\Tests\EntityTranslationUITest;
+
+/**
+ * Tests the Term Translation UI.
+ */
+class TermTranslationUITest extends EntityTranslationUITest {
+
+ /**
+ * The name of the test taxonomy term.
+ */
+ protected $name;
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('language', 'translation_entity', 'taxonomy');
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Taxonomy term translation UI',
+ 'description' => 'Tests the basic term translation UI.',
+ 'group' => 'Taxonomy',
+ );
+ }
+
+ /**
+ * Overrides \Drupal\simpletest\WebTestBase::setUp().
+ */
+ function setUp() {
+ $this->entityType = 'taxonomy_term';
+ $this->bundle = 'tags';
+ $this->name = $this->randomName();
+ parent::setUp();
+ }
+
+ /**
+ * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::setupBundle().
+ */
+ protected function setupBundle() {
+ parent::setupBundle();
+
+ // Create a vocabulary.
+ $vocabulary = entity_create('taxonomy_vocabulary', array(
+ 'name' => $this->bundle,
+ 'description' => $this->randomName(),
+ 'machine_name' => $this->bundle,
+ 'langcode' => LANGUAGE_NOT_SPECIFIED,
+ 'help' => '',
+ 'weight' => mt_rand(0, 10),
+ ));
+ taxonomy_vocabulary_save($vocabulary);
+ }
+
+ /**
+ * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
+ */
+ function getTranslatorPermissions() {
+ return array('administer taxonomy', "translate $this->entityType entities", 'edit original values');
+ }
+
+ /**
+ * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::createEntity().
+ */
+ protected function createEntity($values, $langcode) {
+ $vocabulary = taxonomy_vocabulary_machine_name_load($this->bundle);
+ $values['vid'] = $vocabulary->id();
+ return parent::createEntity($values, $langcode);
+ }
+
+ /**
+ * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues().
+ */
+ protected function getNewEntityValues($langcode) {
+ // Term name is not translatable hence we use a fixed value.
+ return array('name' => $this->name) + parent::getNewEntityValues($langcode);
+ }
+
+}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
index f6e5110..c26251e 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
@@ -98,6 +98,12 @@ class VocabularyFormController extends EntityFormController {
array_unshift($actions['submit']['#submit'],'language_configuration_element_submit');
array_unshift($actions['submit']['#submit'], array($this, 'languageConfigurationSubmit'));
}
+ // We cannot leverage the regular submit handler definition because we
+ // have button-specific ones here. Hence we need to explicitly set it for
+ // the submit action, otherwise it would be ignored.
+ if (module_exists('translation_entity')) {
+ array_unshift($actions['submit']['#submit'], 'translation_entity_language_configuration_element_submit');
+ }
return $actions;
}
else {
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index c92ce64..75573a9 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -357,6 +357,8 @@ function taxonomy_admin_paths() {
$paths = array(
'taxonomy/term/*/edit' => TRUE,
'taxonomy/term/*/delete' => TRUE,
+ 'taxonomy/term/*/translations' => TRUE,
+ 'taxonomy/term/*/translations/*' => TRUE,
);
return $paths;
}
diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php
new file mode 100644
index 0000000..26a2699
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php
@@ -0,0 +1,433 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\translation_entity\EntityTranslationController.
+ */
+
+namespace Drupal\translation_entity;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Base class for entity translation controllers.
+ */
+class EntityTranslationController implements EntityTranslationControllerInterface {
+
+ /**
+ * The type of the entity being translated.
+ *
+ * @var string
+ */
+ protected $entityType;
+
+ /**
+ * The entity info of the entity being translated.
+ *
+ * @var array
+ */
+ protected $entityInfo;
+
+ /**
+ * Initializes an instance of the entity translation controller.
+ *
+ * @param string $entity_type
+ * The type of the entity being translated.
+ * @param array $entity_info
+ * The info array of the given entity type.
+ */
+ public function __construct($entity_type, $entity_info) {
+ $this->entityType = $entity_type;
+ $this->entityInfo = $entity_info;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::removeTranslation().
+ */
+ public function removeTranslation(EntityInterface $entity, $langcode) {
+ $translations = $entity->getTranslationLanguages();
+ // @todo Handle properties.
+ // Remove field translations.
+ foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ if ($field['translatable']) {
+ $entity->{$field_name}[$langcode] = array();
+ }
+ }
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::retranslate().
+ */
+ public function retranslate(EntityInterface $entity, $langcode = NULL) {
+ $updated_langcode = !empty($langcode) ? $langcode : $entity->language()->langcode;
+ $translations = $entity->getTranslationLanguages();
+ foreach ($translations as $langcode => $language) {
+ $entity->retranslate[$langcode] = $langcode != $updated_langcode;
+ }
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getBasePath().
+ */
+ public function getBasePath(EntityInterface $entity) {
+ return $this->getPathInstance($this->entityInfo['menu_base_path'], $entity->id());
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getEditPath().
+ */
+ public function getEditPath(EntityInterface $entity) {
+ return isset($this->entityInfo['menu_edit_path']) ? $this->getPathInstance($this->entityInfo['menu_edit_path'], $entity->id()) : FALSE;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getViewPath().
+ */
+ public function getViewPath(EntityInterface $entity) {
+ return isset($this->entityInfo['menu_view_path']) ? $this->getPathInstance($this->entityInfo['menu_view_path'], $entity->id()) : FALSE;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getAccess().
+ */
+ public function getAccess(EntityInterface $entity, $op) {
+ return TRUE;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getTranslationAccess().
+ */
+ public function getTranslationAccess(EntityInterface $entity, $langcode) {
+ $entity_type = $entity->entityType();
+ return (user_access('translate any entity') || user_access("translate $entity_type entities")) && ($langcode != $entity->language()->langcode || user_access('edit original values'));
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::getSourceLanguage().
+ */
+ public function getSourceLangcode(array $form_state) {
+ return isset($form_state['translation_entity']['source']) ? $form_state['translation_entity']['source']->langcode : FALSE;
+ }
+
+ /**
+ * Implements EntityTranslationControllerInterface::entityFormAlter().
+ */
+ public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $form_langcode = $form_controller->getFormLangcode($form_state);
+ $entity_langcode = $entity->language()->langcode;
+ $source_langcode = $this->getSourceLangcode($form_state);
+
+ $new_translation = !empty($source_langcode);
+ $translations = $entity->getTranslationLanguages();
+ if ($new_translation) {
+ // Make sure a new translation does not appear as existing yet.
+ unset($translations[$form_langcode]);
+ }
+ $is_translation = !$form_controller->isDefaultFormLangcode($form_state);
+ $has_translations = count($translations) > 1;
+
+ // Adjust page title to specify the current language being edited, if we
+ // have at least one translation.
+ $languages = language_list();
+ if (isset($languages[$form_langcode]) && ($has_translations || $new_translation)) {
+ $title = $this->entityFormTitle($entity);
+ // When editing the original values display just the entity label.
+ if ($form_langcode != $entity->language()->langcode) {
+ $t_args = array('%language' => $languages[$form_langcode]->name, '%title' => $entity->label());
+ $title = empty($source_langcode) ? $title . ' [' . t('%language translation', $t_args) . ']' : t('Create %language translation of %title', $t_args);
+ }
+ drupal_set_title($title, PASS_THROUGH);
+ }
+
+ // Display source language selector only if we are creating a new
+ // translation and there are at least two translations available.
+ if ($has_translations && $new_translation) {
+ $form['source_langcode'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Source language: @language', array('@language' => $languages[$source_langcode]->name)),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#tree' => TRUE,
+ '#weight' => -100,
+ '#multilingual' => TRUE,
+ 'source' => array(
+ '#type' => 'select',
+ '#default_value' => $source_langcode,
+ '#options' => array(),
+ ),
+ 'submit' => array(
+ '#type' => 'submit',
+ '#value' => t('Change'),
+ '#submit' => array(array($this, 'entityFormSourceChange')),
+ ),
+ );
+ foreach (language_list(LANGUAGE_CONFIGURABLE) as $language) {
+ if (isset($translations[$language->langcode])) {
+ $form['source_langcode']['source']['#options'][$language->langcode] = $language->name;
+ }
+ }
+ }
+
+ // Disable languages for existing translations, so it is not possible to
+ // switch this node to some language which is already in the translation
+ // set.
+ $language_widget = isset($form['langcode']) && $form['langcode']['#type'] == 'language_select';
+ if ($language_widget && $has_translations) {
+ $form['langcode']['#options'] = array();
+ foreach (language_list(LANGUAGE_CONFIGURABLE) as $language) {
+ if (empty($translations[$language->langcode]) || $language->langcode == $entity_langcode) {
+ $form['langcode']['#options'][$language->langcode] = $language->name;
+ }
+ }
+ }
+
+ if ($is_translation) {
+ if ($language_widget) {
+ $form['langcode']['#access'] = FALSE;
+ }
+
+ // Replace the delete button with the delete translation one.
+ if (!$new_translation) {
+ $weight = 100;
+ foreach (array('delete', 'submit') as $key) {
+ if (isset($form['actions'][$key]['weight'])) {
+ $weight = $form['actions'][$key]['weight'];
+ break;
+ }
+ }
+ $form['actions']['delete_translation'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete translation'),
+ '#weight' => $weight,
+ '#submit' => array(array($this, 'entityFormDeleteTranslation')),
+ );
+ }
+
+ // Always remove the delete button on translation forms.
+ unset($form['actions']['delete']);
+ }
+
+ // We need to display the translation tab only when there is at least one
+ // translation available or a new one is about to be created.
+ if ($new_translation || $has_translations) {
+ $form['translation'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Translation'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#tree' => TRUE,
+ '#weight' => 10,
+ '#access' => $this->getTranslationAccess($entity, $form_langcode),
+ '#multilingual' => TRUE,
+ );
+
+ $translate = !$new_translation && $entity->retranslate[$form_langcode];
+ if (!$translate) {
+ $form['translation']['retranslate'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Flag other translations as outdated'),
+ '#default_value' => FALSE,
+ '#description' => t('If you made a significant change, which means the other translations should be updated, you can flag all translations of this content as outdated. This will not change any other property of them, like whether they are published or not.'),
+ );
+ }
+ else {
+ $form['translation']['translate'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('This translation needs to be updated'),
+ '#default_value' => $translate,
+ '#description' => t('When this option is checked, this translation needs to be updated. Uncheck when the translation is up to date again.'),
+ );
+ }
+
+ if ($language_widget) {
+ $form_langcode['#multilingual'] = TRUE;
+ }
+
+ $form['#process'][] = array($this, 'entityFormSharedElements');
+ }
+
+ // Process the submitted values before they are stored.
+ $form['#entity_builders'][] = array($this, 'entityFormEntityBuild');
+
+ // Handle entity deletion.
+ if (isset($form['actions']['delete'])) {
+ $form['actions']['delete']['#submit'][] = array($this, 'entityFormDelete');
+ }
+ }
+
+ /**
+ * Process callback: Determines which elements get clue in the form.
+ *
+ * @param array $element
+ * Form API element.
+ *
+ * @return array
+ * A processed element with the shared elements marked with a clue.
+ *
+ * @see \Drupal\translation_entity\EntityTranslationController::entityFormAlter()
+ */
+ public function entityFormSharedElements($element) {
+ static $ignored_types;
+
+ // @todo Find a more reliable way to determine if a form element concerns a
+ // multilingual value.
+ if (!isset($ignored_types)) {
+ $ignored_types = array_flip(array('actions', 'value', 'hidden', 'vertical_tabs', 'token'));
+ }
+
+ foreach (element_children($element) as $key) {
+ if (!isset($element[$key]['#type'])) {
+ $this->entityFormSharedElements($element[$key]);
+ }
+ else {
+ // Ignore non-widget form elements.
+ if (isset($ignored_types[$element[$key]['#type']])) {
+ continue;
+ }
+ // Elements are considered to be non multilingual by default.
+ if (empty($element[$key]['#multilingual'])) {
+ $this->addTranslatabilityClue($element[$key]);
+ }
+ }
+ }
+
+ return $element;
+ }
+
+ /**
+ * Adds a clue about the form element translatability.
+ *
+ * If the given element does not have a #title attribute, the function is
+ * recursively applied to child elements.
+ *
+ * @param array $element
+ * A form element array.
+ */
+ protected function addTranslatabilityClue(&$element) {
+ static $suffix, $fapi_title_elements;
+
+ // Elements which can have a #title attribute according to FAPI Reference.
+ if (!isset($suffix)) {
+ $suffix = ' <span class="translation-entity-all-languages">(' . t('all languages') . ')</span>';
+ $fapi_title_elements = array_flip(array('checkbox', 'checkboxes', 'date', 'fieldset', 'file', 'item', 'password', 'password_confirm', 'radio', 'radios', 'select', 'text_format', 'textarea', 'textfield', 'weight'));
+ }
+
+ // Update #title attribute for all elements that are allowed to have a
+ // #title attribute according to the Form API Reference. The reason for this
+ // check is because some elements have a #title attribute even though it is
+ // not rendered, e.g. field containers.
+ if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) {
+ $element['#title'] .= $suffix;
+ }
+ // If the current element does not have a (valid) title, try child elements.
+ elseif ($children = element_children($element)) {
+ foreach ($children as $delta) {
+ $this->addTranslatabilityClue($element[$delta], $suffix);
+ }
+ }
+ // If there are no children, fall back to the current #title attribute if it
+ // exists.
+ elseif (isset($element['#title'])) {
+ $element['#title'] .= $suffix;
+ }
+ }
+
+ /**
+ * Entity builder method.
+ *
+ * @param string $entity_type
+ * The type of the entity.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose form is being built.
+ *
+ * @see \Drupal\translation_entity\EntityTranslationController::entityFormAlter()
+ */
+ public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, array &$form_state) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $form_langcode = $form_controller->getFormLangcode($form_state);
+ $source_langcode = $this->getSourceLangcode($form_state);
+
+ if ($source_langcode) {
+ // @todo Use the entity setter when all entities support multilingual
+ // properties.
+ $entity->source[$form_langcode] = $source_langcode;
+ }
+
+ // Ensure every key has at least a default value. Subclasses may provide
+ // entity-specific values to alter them.
+ $values = isset($form_state['values']['translation']) ? $form_state['values']['translation'] : array();
+ $entity->retranslate[$form_langcode] = isset($values['translate']) && $values['translate'];
+
+ if (!empty($values['retranslate'])) {
+ $this->retranslate($entity, $form_langcode);
+ }
+ }
+
+ /**
+ * Form submission handler for EntityTranslationController::entityFormAlter().
+ *
+ * Takes care of the source language change.
+ */
+ public function entityFormSourceChange($form, &$form_state) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $entity = $form_controller->getEntity($form_state);
+ $source = $form_state['values']['source_langcode']['source'];
+ $path = $this->getBasePath($entity) . '/translations/add/' . $source . '/' . $form_controller->getFormLangcode($form_state);
+ $form_state['redirect'] = array('path' => $path);
+ $languages = language_list();
+ drupal_set_message(t('Source language set to: %language', array('%language' => $languages[$source]->name)));
+ }
+
+ /**
+ * Form submission handler for EntityTranslationController::entityFormAlter().
+ *
+ * Takes care of entity deletion.
+ */
+ function entityFormDelete($form, &$form_state) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $entity = $form_controller->getEntity($form_state);
+ if (count($entity->getTranslationLanguages()) > 1) {
+ drupal_set_message(t('This will delete all the translations of %label.', array('%label' => $entity->label())), 'warning');
+ }
+ }
+
+ /**
+ * Form submission handler for EntityTranslationController::entityFormAlter().
+ *
+ * Takes care of entity translation deletion.
+ */
+ function entityFormDeleteTranslation($form, &$form_state) {
+ $form_controller = translation_entity_form_controller($form_state);
+ $entity = $form_controller->getEntity($form_state);
+ $base_path = $this->getBasePath($entity);
+ $form_langcode = $form_controller->getFormLangcode($form_state);
+ $form_state['redirect'] = $base_path . '/translations/delete/' . $form_langcode;
+ }
+
+ /**
+ * Returns the title to be used for the entity form page.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose form is being altered.
+ */
+ protected function entityFormTitle(EntityInterface $entity) {
+ return $entity->label();
+ }
+
+ /**
+ * Returns an instance of the given path.
+ *
+ * @param $path
+ * An internal path containing the entity id wildcard.
+ *
+ * @return string
+ * The instantiated path.
+ */
+ protected function getPathInstance($path, $entity_id) {
+ $wildcard = $this->entityInfo['menu_path_wildcard'];
+ return str_replace($wildcard, $entity_id, $path);
+ }
+}
diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php
new file mode 100644
index 0000000..ed08fc0
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php
@@ -0,0 +1,190 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\translation_entity\EntityTranslationControllerInterface.
+ */
+
+namespace Drupal\translation_entity;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Interface for providing entity translation.
+ *
+ * Defines a set of methods to allow any entity to be processed by the entity
+ * translation UI.
+ *
+ * The entity translation UI relies on the entity info to provide its features.
+ * See the documentation of hook_entity_info() in the Entity API documentation
+ * for more details on all the entity info keys that may be defined.
+ *
+ * To make Entity Translation automatically support an entity type some keys
+ * may need to be defined, but none of them is required unless the entity path
+ * is different from ENTITY_TYPE/%ENTITY_TYPE (e.g. taxonomy/term/1), in which
+ * case at least the 'menu_base_path' key must be defined. This is used to
+ * determine the view and edit paths if they follow the standard path patterns.
+ * Otherwise the 'menu_view_path' and 'menu_edit_path' keys must be defined. If
+ * an entity type is enabled for translation and no menu path key is defined,
+ * the following defaults will be assumed:
+ * - menu_base_path: ENTITY_TYPE/%ENTITY_TYPE
+ * - menu_view_path: ENTITY_TYPE/%ENTITY_TYPE
+ * - menu_edit_path: ENTITY_TYPE/%ENTITY_TYPE/edit
+ * The menu base path is also used to reliably alter menu router information to
+ * provide the translation overview page for any entity.
+ * If the entity uses a menu loader different from %ENTITY_TYPE also the 'menu
+ * path wildcard' info key needs to be defined.
+ *
+ * Every entity type needs a translation controller to be translated. This can
+ * be specified through the 'translation_controller_class' key in the entity
+ * info. If an entity type is enabled for translation and no translation
+ * controller is defined, Drupal\translation_entity\EntityTranslationController
+ * will be assumed. Every translation controller class must implement
+ * Drupal\translation_entity\EntityTranslationControllerInterface.
+ *
+ * If the entity paths match the default patterns above and there is no need for
+ * an entity-specific translation controller class, Entity Translation will
+ * provide built-in support for the entity. It will still be required to enable
+ * translation for each translatable bundle.
+ *
+ * Additionally some more entity info keys can be defined to further customize
+ * the translation UI. The entity translation info is an associative array that
+ * has to match the following structure. Two nested arrays keyed respectively
+ * by the 'translation' key and the 'entity_translation' key: the first one is
+ * the key defined by the core entity system, while the second one registers
+ * Entity Tanslation as a field translation handler. Elements:
+ * - access callback: The access callback for the translation pages. Defaults to
+ * 'entity_translation_translate_access'.
+ * - access arguments: The access arguments for the translation pages. By
+ * default only the entity object is passed to the access callback.
+ *
+ * This is how entity info would look for a module defining a new translatable
+ * entity type:
+ * @code
+ * function mymodule_entity_info_alter(array &$info) {
+ * $info['myentity'] += array(
+ * 'menu_base_path' => 'mymodule/myentity/%my_entity_loader',
+ * 'menu_path_wildcard' => '%my_entity_loader',
+ * 'translation_controller_class' => 'Drupal\mymodule\MyEntityTranslationController',
+ * 'translation' => array(
+ * 'translation_entity' => array(
+ * 'access_callback' => 'mymodule_myentity_translate_access',
+ * 'access_arguments' => array(2),
+ * ),
+ * ),
+ * );
+ * }
+ * @endcode
+ *
+ * @see \Drupal\Core\Entity\EntityManager
+ */
+interface EntityTranslationControllerInterface {
+
+ /**
+ * Returns the base path for the current entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to the path should refer to.
+ *
+ * @return string
+ * The entity base path.
+ */
+ public function getBasePath(EntityInterface $entity);
+
+ /**
+ * Returns the path of the entity edit form.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to the path should refer to.
+ *
+ * @return string
+ * The entity edit path.
+ */
+ public function getEditPath(EntityInterface $entity);
+
+ /**
+ * Returns the path of the entity view page.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to the path should refer to.
+ *
+ * @return string
+ * The entity view path.
+ */
+ public function getViewPath(EntityInterface $entity);
+
+ /**
+ * Checks if the user can perform the given operation on the wrapped entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity access should be checked for.
+ * @param string $op
+ * The operation to be performed. Possible values are:
+ * - "view"
+ * - "update"
+ * - "delete"
+ * - "create"
+ *
+ * @return
+ * TRUE if the user is allowed to perform the given operation, FALSE
+ * otherwise.
+ */
+ public function getAccess(EntityInterface $entity, $op);
+
+ /**
+ * Checks if a user is allowed to edit the given translation.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose translation has to be accessed.
+ * @param string $langcode
+ * The language code identifying the translation to be accessed.
+ *
+ * @return boolean
+ * TRUE if the operation may be performed, FALSE otherwise.
+ */
+ public function getTranslationAccess(EntityInterface $entity, $langcode);
+
+ /**
+ * Retrieves the source language for the translation being created.
+ *
+ * @param array $form_state
+ * The form state array.
+ *
+ * @return string
+ * The source language code.
+ */
+ public function getSourceLangcode(array $form_state);
+
+ /**
+ * Removes the translation values from the given entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose values should be removed.
+ * @param string $langcode
+ * The language code identifying the translation being deleted.
+ */
+ public function removeTranslation(EntityInterface $entity, $langcode);
+
+ /**
+ * Marks translations as outdated.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity being translated.
+ * @param string $langcode
+ * (optional) The language code of the updated language: all the other
+ * translations will be marked as outdated. Defaults to the entity language.
+ */
+ public function retranslate(EntityInterface $entity, $langcode = NULL);
+
+ /**
+ * Performs the needed alterations to the entity form.
+ *
+ * @param array $form
+ * The entity form to be altered to provide the translation workflow.
+ * @param array $form_state
+ * The form state array.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity being created or edited.
+ */
+ public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity);
+}
diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTestTranslationUITest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTestTranslationUITest.php
new file mode 100644
index 0000000..6f0f9c0
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTestTranslationUITest.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\entity\Tests\EntityTestTranslationUITest.
+ */
+
+namespace Drupal\translation_entity\Tests;
+
+/**
+ * Tests the Entity Test Translation UI.
+ */
+class EntityTestTranslationUITest extends EntityTranslationUITest {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('language', 'translation_entity', 'entity_test');
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Entity Test Translation UI',
+ 'description' => 'Tests the test entity translation UI.',
+ 'group' => 'Entity Translation UI',
+ );
+ }
+
+ /**
+ * Overrides \Drupal\simpletest\WebTestBase::setUp().
+ */
+ function setUp() {
+ $this->entityType = 'entity_test';
+ parent::setUp();
+ }
+
+ /**
+ * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
+ */
+ function getTranslatorPermissions() {
+ return array('administer entity_test content', "translate $this->entityType entities", 'edit original values');
+ }
+
+ /**
+ * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues().
+ */
+ protected function getNewEntityValues($langcode) {
+ return array(
+ 'name' => $this->randomName(),
+ 'user_id' => mt_rand(1, 128),
+ ) + parent::getNewEntityValues($langcode);
+ }
+
+}
diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php
new file mode 100644
index 0000000..934ed62
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php
@@ -0,0 +1,317 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\entity\Tests\EntityTranslationUITest.
+ */
+
+namespace Drupal\translation_entity\Tests;
+
+use Drupal\Core\Entity\DatabaseStorageControllerNG;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityNG;
+use Drupal\Core\Language\Language;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the Entity Translation UI.
+ */
+abstract class EntityTranslationUITest extends WebTestBase {
+
+ /**
+ * The enabled languages.
+ *
+ * @var array
+ */
+ protected $langcodes;
+
+ /**
+ * The entity type being tested.
+ *
+ * @var string
+ */
+ protected $entityType;
+
+ /**
+ * The bundle being tested.
+ *
+ * @var string
+ */
+ protected $bundle;
+
+ /**
+ * The name of the field used to test translation.
+ *
+ * @var string
+ */
+ protected $fieldName;
+
+ /**
+ * Whether the behavior of the language selector should be tested.
+ *
+ * @var boolean
+ */
+ protected $testLanguageSelector = TRUE;
+
+
+ /**
+ * Overrides \Drupal\simpletest\WebTestBase::setUp().
+ */
+ function setUp() {
+ parent::setUp();
+
+ $this->setupLanguages();
+ $this->setupBundle();
+ $this->enableTranslation();
+ $this->setupTranslator();
+ $this->setupTestFields();
+ }
+
+ /**
+ * Enables additional languages.
+ */
+ protected function setupLanguages() {
+ $this->langcodes = array('it', 'fr');
+ foreach ($this->langcodes as $langcode) {
+ language_save(new Language(array('langcode' => $langcode)));
+ }
+ array_unshift($this->langcodes, language_default()->langcode);
+ }
+
+ /**
+ * Creates or initializes the bundle date if needed.
+ */
+ protected function setupBundle() {
+ if (empty($this->bundle)) {
+ $this->bundle = $this->entityType;
+ }
+ }
+
+ /**
+ * Enables translation for the current entity type and bundle.
+ */
+ protected function enableTranslation() {
+ // Enable translation for the current entity type and ensure the change is
+ // picked up.
+ translation_entity_set_config($this->entityType, $this->bundle, 'enabled', TRUE);
+ drupal_static_reset();
+ entity_info_cache_clear();
+ menu_router_rebuild();
+ }
+
+ /**
+ * Returns an array of permissions needed for the translator.
+ */
+ abstract function getTranslatorPermissions();
+
+ /**
+ * Creates and activates a translator user.
+ */
+ protected function setupTranslator() {
+ $translator = $this->drupalCreateUser($this->getTranslatorPermissions());
+ $this->drupalLogin($translator);
+ }
+
+ /**
+ * Creates the test fields.
+ */
+ protected function setupTestFields() {
+ $this->fieldName = 'field_test_et_ui_test';
+
+ $field = array(
+ 'field_name' => $this->fieldName,
+ 'type' => 'text',
+ 'cardinality' => 1,
+ 'translatable' => TRUE,
+ );
+ field_create_field($field);
+
+ $instance = array(
+ 'entity_type' => $this->entityType,
+ 'field_name' => $this->fieldName,
+ 'bundle' => $this->bundle,
+ 'label' => 'Test translatable text-field',
+ 'widget' => array(
+ 'type' => 'text_textfield',
+ 'weight' => 0,
+ ),
+ );
+ field_create_instance($instance);
+ }
+
+ /**
+ * Tests the basic translation UI.
+ */
+ function testTranslationUI() {
+ // Create a new test entity with original values in the default language.
+ $default_langcode = $this->langcodes[0];
+ $values[$default_langcode] = $this->getNewEntityValues($default_langcode);
+ $id = $this->createEntity($values[$default_langcode], $default_langcode);
+ $entity = entity_load($this->entityType, $id, TRUE);
+ $this->assertTrue($entity, t('Entity found in the database.'));
+
+ $translation = $this->getTranslation($entity, $default_langcode);
+ foreach ($values[$default_langcode] as $property => $value) {
+ $stored_value = $this->getValue($translation, $property, $default_langcode);
+ $value = is_array($value) ? $value[0]['value'] : $value;
+ $message = format_string('@property correctly stored in the default language.', array('@property' => $property));
+ $this->assertIdentical($stored_value, $value, $message);
+ }
+
+ // Add an entity translation.
+ $langcode = 'it';
+ $values[$langcode] = $this->getNewEntityValues($langcode);
+
+ $controller = translation_entity_controller($this->entityType);
+ $base_path = $controller->getBasePath($entity);
+ $path = $langcode . '/' . $base_path . '/translations/add/' . $default_langcode . '/' . $langcode;
+ $this->drupalPost($path, $this->getEditValues($values, $langcode), t('Save'));
+ if ($this->testLanguageSelector) {
+ $this->assertNoFieldByXPath('//select[@id="edit-langcode"]', NULL, 'Language selector correclty disabled on translations.');
+ }
+ $entity = entity_load($this->entityType, $entity->id(), TRUE);
+
+ // Switch the source language.
+ $langcode = 'fr';
+ $source_langcode = 'it';
+ $edit = array('source_langcode[source]' => $source_langcode);
+ $path = $langcode . '/' . $base_path . '/translations/add/' . $default_langcode . '/' . $langcode;
+ $this->drupalPost($path, $edit, t('Change'));
+ $this->assertFieldByXPath("//input[@name=\"{$this->fieldName}[fr][0][value]\"]", $values[$source_langcode][$this->fieldName][0]['value'], 'Source language correctly switched.');
+
+ // Add another translation and mark the other ones as outdated.
+ $values[$langcode] = $this->getNewEntityValues($langcode);
+ $edit = $this->getEditValues($values, $langcode) + array('translation[retranslate]' => TRUE);
+ $this->drupalPost($path, $edit, t('Save'));
+ $entity = entity_load($this->entityType, $entity->id(), TRUE);
+
+ // Check that the entered values have been correctly stored.
+ foreach ($values as $langcode => $property_values) {
+ $translation = $this->getTranslation($entity, $langcode);
+ foreach ($property_values as $property => $value) {
+ $stored_value = $this->getValue($translation, $property, $langcode);
+ $value = is_array($value) ? $value[0]['value'] : $value;
+ $message = format_string('%property correctly stored with language %language.', array('%property' => $property, '%language' => $langcode));
+ $this->assertEqual($stored_value, $value, $message);
+ }
+ }
+
+ // Check that every translation has the correct "outdated" status.
+ foreach ($this->langcodes as $enabled_langcode) {
+ $prefix = $enabled_langcode != $default_langcode ? $enabled_langcode . '/' : '';
+ $path = $prefix . $controller->getEditPath($entity);
+ $this->drupalGet($path);
+ if ($enabled_langcode == $langcode) {
+ $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.');
+ }
+ else {
+ $this->assertFieldByXPath('//input[@name="translation[translate]"]', TRUE, 'The translate flag is checked by default.');
+ $edit = array('translation[translate]' => FALSE);
+ $this->drupalPost($path, $edit, t('Save'));
+ $this->drupalGet($path);
+ $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.');
+ $entity = entity_load($this->entityType, $entity->id(), TRUE);
+ $this->assertFalse($entity->retranslate[$enabled_langcode], 'The "outdated" status has been correctly stored.');
+ }
+ }
+
+ // Confirm and delete a translation.
+ $this->drupalPost($path, array(), t('Delete translation'));
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $entity = entity_load($this->entityType, $entity->id(), TRUE);
+ $translations = $entity->getTranslationLanguages();
+ $this->assertTrue(count($translations) == 2 && empty($translations[$enabled_langcode]), 'Translation successfully deleted.');
+ }
+
+ /**
+ * Creates the entity to be translated.
+ *
+ * @param array $values
+ * An array of initial values for the entity.
+ * @param string $langcode
+ * The initial language code of the entity.
+ *
+ * @return
+ * The entity id.
+ */
+ protected function createEntity($values, $langcode) {
+ $entity_values = $values;
+ $entity_values['langcode'] = $langcode;
+ $info = entity_get_info($this->entityType);
+ if (!empty($info['entity_keys']['bundle'])) {
+ $entity_values[$info['entity_keys']['bundle']] = $this->bundle;
+ }
+ $controller = entity_get_controller($this->entityType);
+ if (!($controller instanceof DatabaseStorageControllerNG)) {
+ foreach ($values as $property => $value) {
+ if (is_array($value)) {
+ $entity_values[$property] = array($langcode => $value);
+ }
+ }
+ }
+ $entity = entity_create($this->entityType, $entity_values);
+ $entity->save();
+ return $entity->id();
+ }
+
+ /**
+ * Returns an array of entity field values to be tested.
+ */
+ protected function getNewEntityValues($langcode) {
+ return array($this->fieldName => array(array('value' => $this->randomName(16))));
+ }
+
+ /**
+ * Returns an edit array containing the values to be posted.
+ */
+ protected function getEditValues($values, $langcode, $new = FALSE) {
+ $edit = $values[$langcode];
+ $langcode = $new ? LANGUAGE_NOT_SPECIFIED : $langcode;
+ foreach ($values[$langcode] as $property => $value) {
+ if (is_array($value)) {
+ $edit["{$property}[$langcode][0][value]"] = $value[0]['value'];
+ unset($edit[$property]);
+ }
+ }
+ return $edit;
+ }
+
+ /**
+ * Returns the translation object to use to retrieve the translated values.
+ *
+ * @param \Drupal\Core\Enitity\EntityInterface $entity
+ * The entity being tested.
+ * @param string $langcode
+ * The language code identifying the translation to be retrieved.
+ *
+ * @return \Drupal\Core\TypedData\TranslatableInterface
+ * The translation object to act on.
+ */
+ protected function getTranslation(EntityInterface $entity, $langcode) {
+ return $entity instanceof EntityNG ? $entity->getTranslation($langcode) : $entity;
+ }
+
+ /**
+ * Returns the value for the specified property in the given language.
+ *
+ * @param \Drupal\Core\TypedData\TranslatableInterface $translation
+ * The translation object the property value should be retrieved from.
+ * @param string $property
+ * The property name.
+ * @param string $langcode
+ * The property value.
+ *
+ * @return
+ * The property value.
+ */
+ protected function getValue(ComplexDataInterface $translation, $property, $langcode) {
+ if (($translation instanceof EntityInterface) && !($translation instanceof EntityNG)) {
+ return is_array($translation->$property) ? $translation->{$property}[$langcode][0]['value'] : $translation->$property;
+ }
+ else {
+ return $translation->get($property)->value;
+ }
+ }
+
+}
diff --git a/core/modules/translation_entity/translation_entity.admin.inc b/core/modules/translation_entity/translation_entity.admin.inc
new file mode 100644
index 0000000..bacba11
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.admin.inc
@@ -0,0 +1,262 @@
+<?php
+
+/**
+ * @file
+ * The entity translation administration forms.
+ */
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Form constructor for the confirmation of translatability switching.
+ */
+function translation_entity_translatable_form(array $form, array &$form_state, $field_name) {
+ $field = field_info_field($field_name);
+ $t_args = array('%name' => $field_name);
+
+ $warning = t('By submitting this form these changes will apply to the %name field everywhere it is used.', $t_args);
+ if ($field['translatable']) {
+ $title = t('Are you sure you want to disable translation for the %name field?', $t_args);
+ $warning .= "<br>" . t("<strong>All the existing translations of this field will be deleted.</strong><br>This action cannot be undone.");
+ }
+ else {
+ $title = t('Are you sure you want to enable translation for the %name field?', $t_args);
+ }
+
+ // We need to keep some information for later processing.
+ $form_state['field'] = $field;
+
+ // Store the 'translatable' status on the client side to prevent outdated form
+ // submits from toggling translatability.
+ $form['translatable'] = array(
+ '#type' => 'hidden',
+ '#default_value' => $field['translatable'],
+ );
+
+ return confirm_form($form, $title, '', $warning);
+}
+
+/**
+ * Form submission handler for translation_entity_translatable_form().
+ *
+ * This submit handler maintains consistency between the translatability of an
+ * entity and the language under which the field data is stored. When a field is
+ * marked as translatable, all the data in
+ * $entity->{field_name}[LANGUAGE_NOT_SPECIFIED] is moved to
+ * $entity->{field_name}[$entity_language]. When a field is marked as
+ * untranslatable the opposite process occurs. Note that marking a field as
+ * untranslatable will cause all of its translations to be permanently removed,
+ * with the exception of the one corresponding to the entity language.
+ */
+function translation_entity_translatable_form_submit(array $form, array $form_state) {
+ // This is the current state that we want to reverse.
+ $translatable = $form_state['values']['translatable'];
+ $field_name = $form_state['field']['field_name'];
+ $field = field_info_field($field_name);
+
+ if ($field['translatable'] !== $translatable) {
+ // Field translatability has changed since form creation, abort.
+ $t_args = array('%field_name');
+ $msg = $translatable ?
+ t('The field %field_name is already translatable. No change was performed.', $t_args):
+ t('The field %field_name is already untranslatable. No change was performed.', $t_args);
+ drupal_set_message($msg, 'warning');
+ return;
+ }
+
+ // If a field is untranslatable, it can have no data except under
+ // LANGUAGE_NOT_SPECIFIED. Thus we need a field to be translatable before we convert
+ // data to the entity language. Conversely we need to switch data back to
+ // LANGUAGE_NOT_SPECIFIED before making a field untranslatable lest we lose
+ // information.
+ $operations = array(
+ array('translation_entity_translatable_batch', array(!$translatable, $field_name)),
+ array('translation_entity_translatable_switch', array(!$translatable, $field_name)),
+ );
+ $operations = $translatable ? $operations : array_reverse($operations);
+
+ $t_args = array('%field' => $field_name);
+ $title = !$translatable ? t('Enabling translation for the %field field', $t_args) : t('Disabling translation for the %field field', $t_args);
+
+ $batch = array(
+ 'title' => $title,
+ 'operations' => $operations,
+ 'finished' => 'translation_entity_translatable_batch_done',
+ 'file' => drupal_get_path('module', 'translation_entity') . '/translation_entity.admin.inc',
+ );
+
+ batch_set($batch);
+}
+
+/**
+ * Toggles translatability of the given field.
+ *
+ * This is called from a batch operation, but should only run once per field.
+ *
+ * @param bool $translatable
+ * Indicator of whether the field should be made translatable (TRUE) or
+ * untranslatble (FALSE).
+ * @param string $field_name
+ * Field machine name.
+ */
+function translation_entity_translatable_switch($translatable, $field_name) {
+ $field = field_info_field($field_name);
+
+ if ($field['translatable'] === $translatable) {
+ return;
+ }
+
+ $field['translatable'] = $translatable;
+ field_update_field($field);
+}
+
+/**
+ * Batch callback: Converts field data to or from LANGUAGE_NOT_SPECIFIED.
+ *
+ * @param bool $translatable
+ * Indicator of whether the field should be made translatable (TRUE) or
+ * untranslatble (FALSE).
+ * @param string $field_name
+ * Field machine name.
+ */
+function translation_entity_translatable_batch($translatable, $field_name, &$context) {
+ $entity_types = array();
+
+ // Determine the entity types to act on.
+ foreach (field_info_instances() as $entity_type => $info) {
+ foreach ($info as $bundle => $instances) {
+ foreach ($instances as $instance_field_name => $instance) {
+ if ($instance_field_name == $field_name) {
+ $entity_types[] = $entity_type;
+ break 2;
+ }
+ }
+ }
+ }
+
+ if (empty($context['sandbox'])) {
+ $context['sandbox']['progress'] = 0;
+ $context['sandbox']['max'] = 0;
+
+ foreach ($entity_types as $entity_type) {
+ // How many entities will need processing?
+ $query = entity_query($entity_type);
+ $count = $query
+ ->exists($field_name)
+ ->count()
+ ->execute();
+
+ $context['sandbox']['max'] += $count;
+ $context['sandbox']['progress_entity_type'][$entity_type] = 0;
+ $context['sandbox']['max_entity_type'][$entity_type] = $count;
+ }
+
+ if ($context['sandbox']['max'] === 0) {
+ // Nothing to do.
+ $context['finished'] = 1;
+ return;
+ }
+ }
+
+ foreach ($entity_types as $entity_type) {
+ if ($context['sandbox']['max_entity_type'][$entity_type] === 0) {
+ continue;
+ }
+
+ $info = entity_get_info($entity_type);
+ $offset = $context['sandbox']['progress_entity_type'][$entity_type];
+ $query = entity_query($entity_type);
+ $result = $query
+ ->exists($field_name)
+ ->sort($info['entity_keys']['id'])
+ ->range($offset, 10)
+ ->execute();
+
+ foreach (entity_load_multiple($entity_type, $result) as $id => $entity) {
+ $context['sandbox']['max_entity_type'][$entity_type] -= count($result);
+ $context['sandbox']['progress_entity_type'][$entity_type]++;
+ $context['sandbox']['progress']++;
+ $langcode = $entity->language()->langcode;
+
+ // Skip process for language neutral entities.
+ if ($langcode == LANGUAGE_NOT_SPECIFIED) {
+ continue;
+ }
+
+ // We need a two-step approach while updating field translations: given
+ // that field-specific update functions might rely on the stored values to
+ // perform their processing, see for instance file_field_update(), first
+ // we need to store the new translations and only after we can remove the
+ // old ones. Otherwise we might have data loss, since the removal of the
+ // old translations might occur before the new ones are stored.
+ if ($translatable && isset($entity->{$field_name}[LANGUAGE_NOT_SPECIFIED])) {
+ // If the field is being switched to translatable and has data for
+ // LANGUAGE_NOT_SPECIFIED then we need to move the data to the right
+ // language.
+ $entity->{$field_name}[$langcode] = $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED];
+ // Store the original value.
+ _translation_entity_update_field($entity_type, $entity, $field_name);
+ $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED] = array();
+ // Remove the language neutral value.
+ _translation_entity_update_field($entity_type, $entity, $field_name);
+ }
+ elseif (!$translatable && isset($entity->{$field_name}[$langcode])) {
+ // The field has been marked untranslatable and has data in the entity
+ // language: we need to move it to LANGUAGE_NOT_SPECIFIED and drop the
+ // other translations.
+ $entity->{$field_name}[LANGUAGE_NOT_SPECIFIED] = $entity->{$field_name}[$langcode];
+ // Store the original value.
+ _translation_entity_update_field($entity_type, $entity, $field_name);
+ // Remove translations.
+ foreach ($entity->{$field_name} as $langcode => $items) {
+ if ($langcode != LANGUAGE_NOT_SPECIFIED) {
+ $entity->{$field_name}[$langcode] = array();
+ }
+ }
+ _translation_entity_update_field($entity_type, $entity, $field_name);
+ }
+ else {
+ // No need to save unchanged entities.
+ continue;
+ }
+ }
+ }
+
+ $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+}
+
+/**
+ * Stores the given field translations.
+ */
+function _translation_entity_update_field($entity_type, EntityInterface $entity, $field_name) {
+ $empty = 0;
+ $field = field_info_field($field_name);
+
+ // Ensure that we are trying to store only valid data.
+ foreach ($entity->{$field_name} as $langcode => $items) {
+ $entity->{$field_name}[$langcode] = _field_filter_items($field, $entity->{$field_name}[$langcode]);
+ $empty += empty($entity->{$field_name}[$langcode]);
+ }
+
+ // Save the field value only if there is at least one item available,
+ // otherwise any stored empty field value would be deleted. If this happens
+ // the range queries would be messed up.
+ if ($empty < count($entity->{$field_name})) {
+ field_attach_presave($entity_type, $entity);
+ field_attach_update($entity_type, $entity);
+ }
+}
+
+/**
+ * Batch finished callback: Checks the exit status of the batch operation.
+ */
+function translation_entity_translatable_batch_done($success, $results, $operations) {
+ if ($success) {
+ drupal_set_message(t("Successfully changed field translation setting."));
+ }
+ else {
+ // @todo: Do something about this case.
+ drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."), 'error');
+ }
+}
+
diff --git a/core/modules/translation_entity/translation_entity.info b/core/modules/translation_entity/translation_entity.info
new file mode 100644
index 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..b571da3
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.install
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Installation functions for Entity Translation module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function translation_entity_schema() {
+ $schema['translation_entity'] = array(
+ 'description' => 'Table to track entity translations',
+ 'fields' => array(
+ 'entity_type' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The entity type this translation relates to',
+ ),
+ 'entity_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'description' => 'The entity id this translation relates to',
+ ),
+ 'langcode' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The target language for this translation.',
+ ),
+ 'source' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The source language from which this translation was created.',
+ ),
+ 'translate' => array(
+ 'description' => 'A boolean indicating whether this translation needs to be updated.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'primary key' => array('entity_type', 'entity_id', 'langcode'),
+ );
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function translation_entity_install() {
+ language_negotiation_include();
+ language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LANGUAGE_NEGOTIATION_URL => 0));
+}
+
+/**
+ * Implements hook_enable().
+ */
+function translation_entity_enable() {
+ $t_args = array(
+ '!language_url' => url('admin/config/regional/language'),
+ );
+ $message = t('You just added content translation capabilities to your site. To exploit them be sure to <a href="!language_url">enable at least two languages</a> and enable translation for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em> and any other element whose content you wish to translate.', $t_args);
+ drupal_set_message($message, 'warning');
+}
diff --git a/core/modules/translation_entity/translation_entity.module b/core/modules/translation_entity/translation_entity.module
new file mode 100644
index 0000000..b0c8526
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.module
@@ -0,0 +1,702 @@
+<?php
+
+/**
+ * @file
+ * Allows entities to be translated into different languages.
+ */
+
+use Drupal\Core\Language\Language;
+use Drupal\Core\Entity\EntityFormControllerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityNG;
+
+/**
+ * Implements hook_help().
+ */
+function translation_entity_help($path, $arg) {
+ switch ($path) {
+ case 'admin/help#translation_entity':
+ $output = '';
+ $output .= '<h3>' . t('About') . '</h3>';
+ $output .= '<p>' . t('The Entity Translation module allows you to create and manage translations for your Drupal site content. You can specify which elements need to be translated at the content-type level for content items and comments, at the vocabulary level for taxonomy terms, and at the site level for user accounts. Other modules may provide additional elements that can be translated. For more information, see the online handbook entry for <a href="!url">Entity Translation</a>.', array('!url' => 'http://drupal.org/documentation/modules/entity_translation')) . '</p>';
+ $output .= '<h3>' . t('Uses') . '</h3>';
+ $output .= '<dl>';
+ $output .= '<dt>' . t('Enabling translation') . '</dt>';
+ $output .= '<dd><p>' . t('Before you can translate content, there must be at least two non-system languages added on the <a href="!url">languages administration</a> page.', array('!url' => url('admin/config/regional/language'))) . '</p>';
+ $output .= '<p>' . t('After adding languages, enable translation for any content you wish to translate:') . '</p>';
+ $output .= '<ul><li>' . t('<strong>Content types</strong>: Enable translation of <a href="@content_types_url">content types</a> by clicking edit for to the appropriate type. Then, under Language settings, uncheck <em>Hide language selector</em> and check <em>Enable translation</em>. This allows selecting the language as part of creating or editing the content.', array('@content_types_url' => url('admin/structure/types'))) . '</li>';
+ $output .= '<li>' . t('<strong>Comments</strong>: Switch to the Comment settings in the content type edit page and check <em>Enable translation</em>.</li>');
+ $output .= '<li>' . t('<strong>Taxonomy terms</strong>: Enable translation of <a href="@vocabularies_url">taxonomy</a> terms by editing the Vocabulary and checking <em>Enable translation</em> under Terms language.', array('@vocabularies_url' => url('admin/structure/taxonomy'))) . '</li>';
+ $output .= '<li>' . t('<strong>User accounts</strong>: Enable translation of user accounts on the <a href="@account_url">account settings page</a>, by checking <em>Enable translation</em> under Language settings.', array('@account_url' => url('admin/config/people/accounts'))) . '</li></ul>';
+ $output .= '<p>' . t('Finally, under the <em>Manage fields</em> tab, <em>edit</em> each field you wish to be translatable, and enable translation under <em>Global settings</em>.') . '</p></dd>';
+ $output .= '<dt>' . t('Translating content') . '</dt>';
+ $output .= '<dd>' . t('After enabling translation you can create a new piece of content, or edit existing content and assign it a language. Then, you will see a <em>Translations</em> tab or link that will gives an overview of the translation status for the current content. From there, you can add translations and edit or delete existing translations. This process is similar for every translatable element on your site, such as taxonomy terms, comments or user accounts.') . '</dd>';
+ $output .= '<dt>' . t('Changing source language') . '</dt>';
+ $output .= '<dd>' . t('When there are two or more possible source languages, selecting a <em>Source language</em> will repopulate the form using the specified source\'s values. For example, French is much closer to Spanish than to Chinese, so changing the French translation\'s source language to Spanish can assist translators.') . '</dd>';
+ $output .= '<dt>' . t('Maintaining translations') . '</dt>';
+ $output .= '<dd>' . t('If editing content in one language requires that translated versions also be updated to reflect the change, use the <em>Flag other translations as outdated</em> check box to mark the translations as outdated and in need of revision.') . '</dd>';
+ $output .= '<dt>' . t('Translation permissions') . '</dt>';
+ $output .= '<dd>' . t('The Entity Translation module makes a basic set of permissions available. Additional <a href="@permissions">permissions</a> are made available after translation is enabled for each translatable element.', array('@permissions' => url('admin/people/permissions', array('fragment' => 'module-translation_entity')))) . '</dd>';
+ $output .= '</dl>';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_language_type_info_alter().
+ */
+function translation_entity_language_types_info_alter(array &$language_types) {
+ unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']);
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function translation_entity_entity_info_alter(array &$entity_info) {
+ $edit_form_info = array();
+
+ // Provide defaults for translation info.
+ foreach ($entity_info as $entity_type => &$info) {
+ if (!isset($info['translation']['translation_entity'])) {
+ $info['translation']['translation_entity'] = array();
+ }
+
+ // Every fieldable entity type must have a translation controller class, no
+ // matter if it is enabled for translation or not. As a matter of fact we
+ // might need it to correctly switch field translatability when a field is
+ // shared accross different entities.
+ $info += array('translation_controller_class' => 'Drupal\translation_entity\EntityTranslationController');
+
+ // Check whether translation is enabled at least for one bundle. We cannot
+ // use translation_entity_enabled() here since it would cause infinite
+ // recursion, as it relies on entity info.
+ $enabled = FALSE;
+ $bundles = isset($info['bundles']) ? array_keys($info['bundles']) : array($entity_type);
+ foreach ($bundles as $bundle) {
+ if (translation_entity_get_config($entity_type, $bundle, 'enabled')) {
+ $enabled = TRUE;
+ break;
+ }
+ }
+
+ if ($enabled) {
+ // If no menu base path is provided we default to the usual
+ // "entity_type/%entity_type" pattern.
+ if (!isset($info['menu_base_path'])) {
+ $path = "$entity_type/%$entity_type";
+ $info['menu_base_path'] = $path;
+ }
+
+ $path = $info['menu_base_path'];
+
+ $info += array(
+ 'menu_view_path' => $path,
+ 'menu_edit_path' => "$path/edit",
+ 'menu_path_wildcard' => "%$entity_type",
+ );
+
+ $entity_position = count(explode('/', $path)) - 1;
+ $info['translation']['translation_entity'] += array(
+ 'access_callback' => 'translation_entity_translate_access',
+ 'access_arguments' => array($entity_position),
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function translation_entity_menu() {
+ $items = array();
+
+ // Create tabs for all possible entity types.
+ foreach (entity_get_info() as $entity_type => $info) {
+ // Provide the translation UI only for enabled types.
+ if (translation_entity_enabled($entity_type)) {
+ $path = $info['menu_base_path'];
+ $entity_position = count(explode('/', $path)) - 1;
+ $keys = array_flip(array('theme_callback', 'theme_arguments', 'access_callback', 'access_arguments', 'load_arguments'));
+ $menu_info = array_intersect_key($info['translation']['translation_entity'], $keys) + array('file' => 'translation_entity.pages.inc');
+ $item = array();
+
+ // Plugin annotations cannot contain spaces, thus we need to restore them
+ // from underscores.
+ foreach ($menu_info as $key => $value) {
+ $item[str_replace('_', ' ', $key)] = $value;
+ }
+
+ $items["$path/translations"] = array(
+ 'title' => 'Translations',
+ 'page callback' => 'translation_entity_overview',
+ 'page arguments' => array($entity_position),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 2,
+ ) + $item;
+
+ // Add translation callback.
+ // @todo Add the access callback instead of replacing it as soon as the
+ // routing system supports multiple callbacks.
+ $add_path = "$path/translations/add/%language/%language";
+ $language_position = $entity_position + 3;
+ $args = array($entity_position, $language_position, $language_position + 1);
+ $items[$add_path] = array(
+ 'title' => 'Add',
+ 'page callback' => 'translation_entity_add_page',
+ 'page arguments' => $args,
+ 'access callback' => 'translation_entity_add_access',
+ 'access arguments' => $args,
+ ) + $item;
+
+ // Delete translation callback.
+ $items["$path/translations/delete/%language"] = array(
+ 'title' => 'Delete',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('translation_entity_delete_confirm', $entity_position, $language_position),
+ ) + $item;
+ }
+ }
+
+ $items['admin/config/regional/translation_entity/translatable/%'] = array(
+ 'title' => 'Confirm change in translatability.',
+ 'description' => 'Confirm page for changing field translatability.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('translation_entity_translatable_form', 5),
+ 'access arguments' => array('toggle field translatability'),
+ 'file' => 'translation_entity.admin.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_menu_alter().
+ */
+function translation_entity_menu_alter(array &$items) {
+ // Some menu loaders in the item paths might have been altered: we need to
+ // replace any menu loader with a plain % to check if base paths are still
+ // compatible.
+ $paths = array();
+ $regex = '|%[^/]+|';
+ foreach ($items as $path => $item) {
+ $path = preg_replace($regex, '%', $path);
+ $paths[$path] = $path;
+ }
+
+ // Check that the declared menu base paths are actually valid.
+ foreach (entity_get_info() as $entity_type => $info) {
+ if (translation_entity_enabled($entity_type)) {
+ $path = $info['menu_base_path'];
+
+ // If the base path is not defined or is not compatible with any defined
+ // one we cannot provide the translation UI for this entity type.
+ if (!isset($paths[preg_replace($regex, '%', $path)])) {
+ drupal_set_message(t('The entities of type %entity_type do not define a valid base path: it will not be possible to translate them.', array('%entity_type' => $info['label'])), 'warning');
+ unset(
+ $items["$path/translations"],
+ $items["$path/translations/add/%language"],
+ $items["$path/translations/delete/%language"]
+ );
+ }
+ else {
+ $entity_position = count(explode('/', $path)) - 1;
+ $edit_path = $info['menu_edit_path'];
+
+ if (isset($items[$edit_path])) {
+ // If the edit path is a default local task we need to find the parent
+ // item.
+ $edit_path_split = explode('/', $edit_path);
+ do {
+ $entity_form_item = &$items[implode('/', $edit_path_split)];
+ array_pop($edit_path_split);
+ }
+ while (!empty($entity_form_item['type']) && $entity_form_item['type'] == MENU_DEFAULT_LOCAL_TASK);
+
+ // Make the "Translate" tab follow the "Edit" one when possibile.
+ if (isset($entity_form_item['weight'])) {
+ $items["$path/translations"]['weight'] = $entity_form_item['weight'] + 0.01;
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Access callback for the translation overview page.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose translation overview should be displayed.
+ */
+function translation_entity_translate_access(EntityInterface $entity) {
+ $entity_type = $entity->entityType();
+ return empty($entity->language()->locked) && language_multilingual() && translation_entity_enabled($entity_type, $entity->bundle()) && (user_access('translate any entity') || user_access("translate $entity_type entities"));
+}
+
+/**
+ * Access callback for the translation addition page.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity being translated.
+ * @param \Drupal\Core\Language\Language $source
+ * The language of the values being translated.
+ * @param \Drupal\Core\Language\Language $target
+ * The language of the translated values.
+ */
+function translation_entity_add_access(EntityInterface $entity, Language $source = NULL, Language $target = NULL) {
+ $source = !empty($source) ? $source : $entity->language();
+ $target = !empty($target) ? $target : language(LANGUAGE_TYPE_CONTENT);
+ $translations = $entity->getTranslationLanguages();
+ $languages = language_list();
+ return $source->langcode != $target->langcode && isset($languages[$source->langcode]) && isset($languages[$target->langcode]) && !isset($translations[$target->langcode]) && translation_entity_access($entity, $target->langcode);
+}
+
+/**
+ * Returns the key name used to store the configuration item.
+ *
+ * Based on the entity type and bundle, the variables used to store the
+ * configuration will have a common root name.
+ *
+ * @param string $entity_type
+ * The type of the entity the setting refers to.
+ * @param string $bundle
+ * The bundle of the entity the setting refers to.
+ * @param string $setting
+ * The name of the setting.
+ *
+ * @return string
+ * The key name of the configuration item.
+ *
+ * @todo Generalize this logic so that it is available to any module needing
+ * per-bundle configuration.
+ */
+function translation_entity_get_config_key($entity_type, $bundle, $setting) {
+ $entity_type = preg_replace('/[^0-9a-zA-Z_]/', "_", $entity_type);
+ $bundle = preg_replace('/[^0-9a-zA-Z_]/', "_", $bundle);
+ return $entity_type . '.' . $bundle . '.translation_entity.' . $setting;
+}
+
+/**
+ * Retrieves the value for the specified setting.
+ *
+ * @param string $entity_type
+ * The type of the entity the setting refer to.
+ * @param string $bundle
+ * The bundle of the entity the setting refer to.
+ * @param string $setting
+ * The name of the setting.
+ *
+ * @returns mixed
+ * The stored value for the given setting.
+ */
+function translation_entity_get_config($entity_type, $bundle, $setting) {
+ $key = translation_entity_get_config_key($entity_type, $bundle, $setting);
+ return config('translation_entity.settings')->get($key);
+}
+
+/**
+ * Stores the given value for the specified setting.
+ *
+ * @param string $entity_type
+ * The type of the entity the setting refer to.
+ * @param string $bundle
+ * The bundle of the entity the setting refer to.
+ * @param string $setting
+ * The name of the setting.
+ * @param $value
+ * The value to be stored for the given setting.
+ */
+function translation_entity_set_config($entity_type, $bundle, $setting, $value) {
+ $key = translation_entity_get_config_key($entity_type, $bundle, $setting);
+ return config('translation_entity.settings')->set($key, $value)->save();
+}
+
+/**
+ * Determines whether the given entity type is translatable.
+ *
+ * @param string $entity_type
+ * The type of the entity.
+ * @param string $bundle
+ * (optional) The bundle of the entity. If no bundle is provided, all the
+ * available bundles are checked.
+ * @param boolean $skip_handler
+ * (optional) Specifies whether the availablity of a field translation handler
+ * should affect the returned value. By default the check is performed.
+ *
+ * @returns
+ * TRUE if the specified bundle is translatable. If no bundle is provided
+ * returns TRUE if at least one of the entity bundles is translatable.
+ */
+function translation_entity_enabled($entity_type, $bundle = NULL, $skip_handler = FALSE) {
+ $enabled = FALSE;
+ $bundles = !empty($bundle) ? array($bundle) : entity_get_bundles($entity_type);
+
+ foreach ($bundles as $bundle) {
+ if (translation_entity_get_config($entity_type, $bundle, 'enabled')) {
+ $enabled = TRUE;
+ break;
+ }
+ }
+
+ return $enabled && ($skip_handler || field_has_translation_handler($entity_type, 'translation_entity'));
+}
+
+/**
+ * Entity translation controller factory.
+ *
+ * @param string $entity_type
+ * The type of the entity being translated.
+ *
+ * @return \Drupal\translation_entity\EntityTranslationControllerInterface
+ * An instance of the entity translation controller interface.
+ */
+function translation_entity_controller($entity_type) {
+ $entity_info = entity_get_info($entity_type);
+ // @todo Throw an exception if the key is missing.
+ return new $entity_info['translation_controller_class']($entity_type, $entity_info);
+}
+
+/**
+ * Returns the entity form controller for the given form.
+ *
+ * @param array $form_state
+ * The form state array holding the entity form controller.
+ *
+ * @return \Drupal\Core\Entity\EntityFormControllerInterface;
+ * An instance of the entity translation form interface or FALSE if not an
+ * entity form.
+ */
+function translation_entity_form_controller(array $form_state) {
+ return isset($form_state['controller']) && $form_state['controller'] instanceof EntityFormControllerInterface ? $form_state['controller'] : FALSE;
+}
+
+/**
+ * Checks whether an entity translation is accessible.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be accessed.
+ * @param string $langcode
+ * The language of the translation to be accessed.
+ *
+ * @return
+ * TRUE if the current user is allowed to view the translation.
+ */
+function translation_entity_access(EntityInterface $entity, $langcode) {
+ return translation_entity_controller($entity->entityType())->getTranslationAccess($entity, $langcode) ;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function translation_entity_permission() {
+ $permission = array(
+ 'edit original values' => array(
+ 'title' => t('Edit original values'),
+ 'description' => t('Access the entity form in the original language.'),
+ ),
+ 'toggle field translatability' => array(
+ 'title' => t('Toggle field translatability'),
+ 'description' => t('Toggle translatability of fields performing a bulk update.'),
+ ),
+ 'translate any entity' => array(
+ 'title' => t('Translate any entity'),
+ 'description' => t('Translate field content for any fieldable entity.'),
+ ),
+ );
+
+ foreach (entity_get_info() as $entity_type => $info) {
+ if (translation_entity_enabled($entity_type)) {
+ $label = !empty($info['label']) ? t($info['label']) : $entity_type;
+ $permission["translate $entity_type entities"] = array(
+ 'title' => t('Translate entities of type @type', array('@type' => $label)),
+ 'description' => t('Translate field content for entities of type @type.', array('@type' => $label)),
+ );
+ }
+ }
+
+ return $permission;
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function translation_entity_form_alter(array &$form, array &$form_state) {
+ if (($form_controller = translation_entity_form_controller($form_state)) && ($entity = $form_controller->getEntity($form_state)) && !$entity->isNew()) {
+ $controller = translation_entity_controller($entity->entityType());
+ $controller->entityFormAlter($form, $form_state, $entity);
+
+ // @todo Move the following lines to the code generating the property form
+ // elements once we have an official #multilingual FAPI key.
+ $translations = $entity->getTranslationLanguages();
+ $form_langcode = $form_controller->getFormLangcode($form_state);
+
+ // Handle fields shared between translations when there is at least one
+ // translation available or a new one is being created.
+ if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) {
+ if ($entity instanceof EntityNG) {
+ foreach ($entity->getPropertyDefinitions() as $property_name => $definition) {
+ if (isset($form[$property_name])) {
+ $form[$property_name]['#multilingual'] = !empty($definition['translatable']);
+ }
+ }
+ }
+ else {
+ foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ $form[$field_name]['#multilingual'] = !empty($field['translatable']);
+ }
+ }
+ }
+
+ }
+}
+
+/**
+ * Implements hook_entity_load().
+ */
+function translation_entity_entity_load(array $entities, $entity_type) {
+ $enabled_entities = array();
+
+ if (translation_entity_enabled($entity_type)) {
+ foreach ($entities as $entity) {
+ if (translation_entity_enabled($entity_type, $entity->bundle())) {
+ $enabled_entities[$entity->id()] = $entity;
+ }
+ }
+ }
+
+ if (!empty($enabled_entities)) {
+ translation_entity_load_translation_data($enabled_entities, $entity_type);
+ }
+}
+
+/**
+ * Loads translation data into the given entities.
+ *
+ * @param array $entities
+ * The entities keyed by entity ID.
+ * @param string $entity_type
+ * The type of the entities.
+ */
+function translation_entity_load_translation_data(array $entities, $entity_type) {
+ $result = db_select('translation_entity', 'te')
+ ->fields('te', array())
+ ->condition('te.entity_type', $entity_type)
+ ->condition('te.entity_id', array_keys($entities))
+ ->execute();
+
+ foreach ($result as $record) {
+ $entity = $entities[$record->entity_id];
+ // @todo Declare these as entity (translation?) properties.
+ $entity->source[$record->langcode] = $record->source;
+ // @todo Rename to 'translate' when the column is removed from the node
+ // schema.
+ $entity->retranslate[$record->langcode] = (boolean) $record->translate;
+ }
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function translation_entity_entity_insert(EntityInterface $entity) {
+ $entity_type = $entity->entityType();
+ $id = $entity->id();
+ $query = db_insert('translation_entity')
+ ->fields(array('entity_type', 'entity_id', 'langcode', 'source', 'translate'));
+
+ foreach ($entity->getTranslationLanguages() as $langcode => $language) {
+ // @todo Declare these as entity (translation?) properties.
+ $source = (isset($entity->source[$langcode]) ? $entity->source[$langcode] : NULL) . '';
+ $retranslate = intval(!empty($entity->retranslate[$langcode]));
+ $query->values(array($entity_type, $id, $langcode, $source, $retranslate));
+ }
+
+ $query->execute();
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function translation_entity_entity_delete(EntityInterface $entity) {
+ db_delete('translation_entity')
+ ->condition('entity_type', $entity->entityType())
+ ->condition('entity_id', $entity->id())
+ ->execute();
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function translation_entity_entity_update(EntityInterface $entity) {
+ // Delete and create to ensure no stale value remains behind.
+ translation_entity_entity_delete($entity);
+ translation_entity_entity_insert($entity);
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ */
+function translation_entity_field_extra_fields() {
+ $extra = array();
+
+ foreach (entity_get_info() as $entity_type => $info) {
+ foreach (entity_get_bundles($entity_type) as $bundle) {
+ if (translation_entity_enabled($entity_type, $bundle)) {
+ $extra[$entity_type][$bundle]['form']['translation'] = array(
+ 'label' => t('Translation'),
+ 'description' => t('Translation settings'),
+ 'weight' => 10,
+ );
+ }
+ }
+ }
+
+ return $extra;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function translation_entity_form_field_ui_field_edit_form_alter(array &$form, array &$form_state, $form_id) {
+ $field = $form['#field'];
+ $field_name = $field['field_name'];
+ $translatable = $field['translatable'];
+ $label = t('Field translation');
+ $title = t('Users may translate this field.');
+
+ $form['field']['#collapsed'] = $translatable;
+
+ if (field_has_data($field)) {
+ $path = "admin/config/regional/translation_entity/translatable/$field_name";
+ $status = $translatable ? $title : t('This field has data in existing content.');
+ $link_title = !$translatable ? t('Enable translation') : t('Disable translation');
+
+ $form['field']['translatable'] = array(
+ '#prefix' => '<div class="translatable"><label>' . $label . '</label>',
+ '#suffix' => '</div>',
+ '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' => '<label>' . $label . '</label>',
+ '#type' => 'checkbox',
+ '#title' => $title,
+ '#default_value' => $translatable,
+ );
+ }
+}
+
+/**
+ * Implements hook_element_info_alter().
+ */
+function translation_entity_element_info_alter(&$type) {
+ if (isset($type['language_configuration'])) {
+ $type['language_configuration']['#process'][] = 'translation_entity_language_configuration_element_process';
+ }
+}
+/**
+ * Returns a widget to enable entity translation per entity bundle.
+ *
+ * Backward compatibility layer to support entities not using the language
+ * configuration form element.
+ *
+ * @todo Remove once all core entities have language configuration.
+ *
+ * @param string $entity_type
+ * The type of the entity being configured for translation.
+ * @param string $bundle
+ * The bundle of the entity being configured for translation.
+ * @param array $form
+ * The configuration form array.
+ * @param array $form_state
+ * The configuration form state array.
+ */
+function translation_entity_enable_widget($entity_type, $bundle, array &$form, array &$form_state) {
+ $key = $form_state['translation_entity']['key'];
+ if (!isset($form_state['language'][$key])) {
+ $form_state['language'][$key] = array();
+ }
+ $form_state['language'][$key] += array('entity_type' => $entity_type, 'bundle' => $bundle);
+ $element = translation_entity_language_configuration_element_process(array('#name' => $key), $form_state, $form);
+ unset($element['translation_entity']['#element_validate']);
+ return $element;
+}
+
+/**
+ * Process callback: Expands the language_configuration form element.
+ *
+ * @param array $element
+ * Form API element.
+ *
+ * @return
+ * Processed language configuration element.
+ */
+function translation_entity_language_configuration_element_process(array $element, array &$form_state, array &$form) {
+ $form_state['translation_entity']['key'] = $element['#name'];
+ $context = $form_state['language'][$element['#name']];
+
+ $element['translation_entity'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable translation'),
+ '#default_value' => translation_entity_enabled($context['entity_type'], $context['bundle']),
+ '#element_validate' => array('translation_entity_language_configuration_element_validate'),
+ '#prefix' => '<label>' . t('Translation') . '</label>',
+ );
+
+ $form['#submit'][] = 'translation_entity_language_configuration_element_submit';
+
+ return $element;
+}
+
+/**
+ * Form validation handler for element added with translation_entity_language_configuration_element_process().
+ *
+ * Checks whether translation can be enabled: if language is set to one of the
+ * special languages and language selector is not hidden, translation cannot be
+ * enabled.
+ *
+ * @see translation_entity_language_configuration_element_submit()
+ */
+function translation_entity_language_configuration_element_validate($element, array &$form_state, array $form) {
+ $key = $form_state['translation_entity']['key'];
+ $values = $form_state['values'][$key];
+ if (language_is_locked($values['langcode']) && $values['language_hidden'] && $values['translation_entity']) {
+ foreach (language_list(LANGUAGE_LOCKED) as $language) {
+ $locked_languages[] = $language->name;
+ }
+ // @todo Set the correct form element name as soon as the element parents
+ // are correctly set. We should be using NestedArray::getValue() but for
+ // now we cannot.
+ form_set_error('', t('Translation is not supported if language is always one of: @locked_languages', array('@locked_languages' => implode(', ', $locked_languages))));
+ }
+}
+
+/**
+ * Form submission handler for element added with translation_entity_language_configuration_element_process().
+ *
+ * Stores the entity translation settings.
+ *
+ * @see translation_entity_language_configuration_element_validate()
+ */
+function translation_entity_language_configuration_element_submit(array $form, array &$form_state) {
+ $key = $form_state['translation_entity']['key'];
+ $context = $form_state['language'][$key];
+ $enabled = $form_state['values'][$key]['translation_entity'];
+
+ if (translation_entity_enabled($context['entity_type'], $context['bundle']) != $enabled) {
+ translation_entity_set_config($context['entity_type'], $context['bundle'], 'enabled', $enabled);
+ entity_info_cache_clear();
+ menu_router_rebuild();
+ }
+}
diff --git a/core/modules/translation_entity/translation_entity.pages.inc b/core/modules/translation_entity/translation_entity.pages.inc
new file mode 100644
index 0000000..be06e6a
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.pages.inc
@@ -0,0 +1,263 @@
+<?php
+
+/**
+ * @file
+ * The entity translation user interface.
+ */
+
+use Drupal\Core\Language\Language;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityNG;
+
+/**
+ * Translations overview page callback.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose translation overview should be displayed.
+ */
+function translation_entity_overview(EntityInterface $entity) {
+ $controller = translation_entity_controller($entity->entityType());
+ $languages = language_list();
+ $original = $entity->language()->langcode;
+ $translations = $entity->getTranslationLanguages();
+ $field_ui = module_exists('field_ui');
+
+ $path = $controller->getViewPath($entity);
+ $base_path = $controller->getBasePath($entity);
+ $edit_path = $controller->getEditPath($entity);
+
+ $header = array(t('Language'), t('Translation'), t('Source language'), t('Status'), t('Operations'));
+ $rows = array();
+
+ if (language_multilingual()) {
+ // If we have a view path defined for the current entity get the switch
+ // links based on it.
+ if ($path) {
+ $links = _translation_entity_get_switch_links($path);
+ }
+
+ // Determine whether the current entity is translatable.
+ $translatable = FALSE;
+ foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ if ($field['translatable']) {
+ $translatable = TRUE;
+ break;
+ }
+ }
+
+ foreach ($languages as $language) {
+ $language_name = $language->name;
+ $langcode = $language->langcode;
+ $add_path = $base_path . '/translations/add/' . $original . '/' . $langcode;
+ $delete_path = $base_path . '/translations/delete/' . $langcode;
+
+ if ($base_path) {
+ $add_links = _translation_entity_get_switch_links($add_path);
+ $edit_links = _translation_entity_get_switch_links($edit_path);
+ $delete_links = _translation_entity_get_switch_links($delete_path);
+ }
+
+ $operations = array(
+ 'data' => array(
+ '#type' => 'operations',
+ '#links' => array(),
+ ),
+ );
+ $links = &$operations['data']['#links'];
+
+ if (isset($translations[$langcode])) {
+ // Existing translation in the translation set: display status.
+ $source = isset($entity->source[$langcode]) ? $entity->source[$langcode] : '';
+ $is_original = $langcode == $original;
+ $translation = $translations[$langcode];
+ $label = $entity->label($langcode);
+ $link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $language);
+ $row_title = l($label, $link['href'], $link);
+
+ if (empty($link['href'])) {
+ $row_title = $is_original ? $label : t('n/a');
+ }
+
+ if ($edit_path && $controller->getAccess($entity, 'update') && $controller->getTranslationAccess($entity, $langcode)) {
+ $links['edit'] = isset($edit_links->links[$langcode]['href']) ? $edit_links->links[$langcode] : array('href' => $edit_path, 'language' => $language);
+ $links['edit']['title'] = t('edit');
+ }
+
+ // @todo Consider supporting the ability to track translation publishing
+ // status independently from entity status, as it may not exist.
+ $translation = $entity->getTranslation($langcode, FALSE);
+ $status = !isset($translation->status) || $translation->status ? t('Published') : t('Not published');
+ // @todo Add a theming function here.
+ $status = '<span class="status">' . $status . '</span>' . (!empty($entity->retranslate[$langcode]) ? ' <span class="marker">' . t('outdated') . '</span>' : '');
+
+ if ($is_original) {
+ $language_name = t('<strong>@language_name</strong>', array('@language_name' => $language_name));
+ $source_name = t('n/a');
+ }
+ else {
+ $source_name = isset($languages[$source]) ? $languages[$source]->name : t('n/a');
+ $links['delete'] = isset($delete_links->links[$langcode]['href']) ? $delete_links->links[$langcode] : array('href' => $delete_links, 'language' => $language);
+ $links['delete']['title'] = t('delete');
+ }
+ }
+ else {
+ // No such translation in the set yet: help user to create it.
+ $row_title = $source_name = t('n/a');
+ $source = $entity->language()->langcode;
+
+ if ($source != $langcode && $controller->getAccess($entity, 'update')) {
+ if ($translatable) {
+ $links['add'] = isset($add_links->links[$langcode]['href']) ? $add_links->links[$langcode] : array('href' => $add_path, 'language' => $language);
+ $links['add']['title'] = t('add');
+ }
+ elseif ($field_ui) {
+ $entity_path = _field_ui_bundle_admin_path($entity->entityType(), $entity->bundle());
+ // Link directly to the fields tab to make it easier to find the
+ // setting to enable translation on fields.
+ $path = $entity_path . '/fields';
+ $links['nofields'] = array('title' => t('no translatable fields'), 'href' => $path, 'language' => $language);
+ }
+ }
+
+ $status = t('Not translated');
+ }
+
+ $rows[] = array($language_name, $row_title, $source_name, $status, $operations);
+ }
+ }
+
+ drupal_set_title(t('Translations of %label', array('%label' => $entity->label())), PASS_THROUGH);
+
+ // Add metadata to the build render array to let other modules know about
+ // which entity this is.
+ $build['#entity'] = $entity;
+
+ $build['translation_entity_overview'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ );
+
+ return $build;
+}
+
+/**
+ * Returns the localized links for the given path.
+ *
+ * @param string $path
+ * The path for which language switch links should be provided.
+ *
+ * @returns
+ * A renderable array of language switch links.
+ */
+function _translation_entity_get_switch_links($path) {
+ $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path);
+ if (empty($links)) {
+ // If content language is set up to fall back to the interface language,
+ // then there will be no switch links for LANGUAGE_TYPE_CONTENT, ergo we
+ // also need to use interface switch links.
+ $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_INTERFACE, $path);
+ }
+ return $links;
+}
+
+/**
+ * Page callback for the translation addition page.
+ *
+ * @param EntityInterface $entity
+ * The entity being translated.
+ * @param Language $source
+ * (optional) The language of the values being translated. Defaults to the
+ * entity language.
+ * @param Language $target
+ * (optional) The language of the translated values. Defaults to the current
+ * content language.
+ *
+ * @return array
+ * A processed form array ready to be rendered.
+ */
+function translation_entity_add_page(EntityInterface $entity, Language $source = NULL, Language $target = NULL) {
+ $source = !empty($source) ? $source : $entity->language();
+ $target = !empty($target) ? $target : language(LANGUAGE_TYPE_CONTENT);
+ // @todo Exploit the upcoming hook_entity_prepare() when available.
+ translation_entity_prepare_translation($entity, $source, $target);
+ $info = $entity->entityInfo();
+ $operation = isset($info['default_operation']) ? $info['default_operation'] : 'default';
+ $form_state = entity_form_state_defaults($entity, $operation, $target->langcode);
+ $form_state['translation_entity']['source'] = $source;
+ $form_state['translation_entity']['target'] = $target;
+ $form_id = entity_form_id($entity);
+ return drupal_build_form($form_id, $form_state);
+}
+
+/**
+ * Populates target values with the source values.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entitiy being translated.
+ * @param \Drupal\Core\Language\Language $source
+ * The language to be used as source.
+ * @param \Drupal\Core\Language\Language $target
+ * The language to be used as target.
+ */
+function translation_entity_prepare_translation(EntityInterface $entity, Language $source, Language $target) {
+ // @todo Unify field and property handling.
+ $instances = field_info_instances($entity->entityType(), $entity->bundle());
+ if ($entity instanceof EntityNG) {
+ $source_translation = $entity->getTranslation($source->langcode);
+ $target_translation = $entity->getTranslation($target->langcode);
+ foreach ($target_translation->getPropertyDefinitions() as $property_name => $definition) {
+ // @todo The value part should not be needed. Remove it as soon as things
+ // do not break.
+ $target_translation->$property_name->value = $source_translation->$property_name->value;
+ }
+ }
+ else {
+ foreach ($instances as $field_name => $instance) {
+ $field = field_info_field($field_name);
+ if (!empty($field['translatable'])) {
+ $value = $entity->get($field_name);
+ $value[$target->langcode] = isset($value[$source->langcode]) ? $value[$source->langcode] : array();
+ $entity->set($field_name, $value);
+ }
+ }
+ }
+}
+
+/**
+ * Form constructor for the translation deletion confirmation.
+ */
+function translation_entity_delete_confirm(array $form, array $form_state, EntityInterface $entity, Language $language) {
+ $langcode = $language->langcode;
+ $controller = translation_entity_controller($entity->entityType());
+
+ return confirm_form(
+ $form,
+ t('Are you sure you want to delete the @language translation of %label?', array('@language' => $language->name, '%label' => $entity->label())),
+ $controller->getEditPath($entity),
+ t('This action cannot be undone.'),
+ t('Delete'),
+ t('Cancel')
+ );
+}
+
+/**
+ * Form submission handler for translation_entity_delete_confirm().
+ */
+function translation_entity_delete_confirm_submit(array $form, array &$form_state) {
+ list($entity, $language) = $form_state['build_info']['args'];
+ $controller = translation_entity_controller($entity->entityType());
+
+ // Remove the translated values.
+ $controller->removeTranslation($entity, $language->langcode);
+ $entity->save();
+
+ // Remove any existing path alias for the removed translation.
+ if (module_exists('path')) {
+ path_delete(array('source' => $controller->getViewPath($entity), 'langcode' => $language->langcode));
+ }
+
+ $form_state['redirect'] = $controller->getBasePath($entity) . '/translations';
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
index 68edca1..dbedf89 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/Core/Entity/User.php
@@ -24,6 +24,8 @@ use Drupal\Core\Annotation\Translation;
* "profile" = "Drupal\user\ProfileFormController",
* "register" = "Drupal\user\RegisterFormController"
* },
+ * default_operation = "profile",
+ * translation_controller_class = "Drupal\user\ProfileTranslationController",
* base_table = "users",
* uri_callback = "user_uri",
* label_callback = "user_label",
diff --git a/core/modules/user/lib/Drupal/user/ProfileTranslationController.php b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php
new file mode 100644
index 0000000..582644b
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\ProfileTranslationController.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\translation_entity\EntityTranslationController;
+
+/**
+ * Defines the translation controller class for terms.
+ */
+class ProfileTranslationController extends EntityTranslationController {
+
+ /**
+ * Overrides EntityTranslationController::entityFormAlter().
+ */
+ public function entityFormAlter(array &$form, array &$form_state, EntityInterface $entity) {
+ parent::entityFormAlter($form, $form_state, $entity);
+ $form['actions']['submit']['#submit'][] = array($this, 'entityFormSave');
+ }
+
+ /**
+ * Form submission handler for ProfileTranslationController::entityFormAlter().
+ *
+ * This handles the save action.
+ *
+ * @see \Drupal\Core\Entity\EntityFormController::build().
+ */
+ function entityFormSave(array $form, array &$form_state) {
+ if ($this->getSourceLangcode($form_state)) {
+ $entity = translation_entity_form_controller($form_state)->getEntity($form_state);
+ // We need a redirect here, otherwise we would get an access denied page
+ // since the current URL would be preserved and we would try to add a
+ // translation for a language that already has a translation.
+ $form_state['redirect'] = $this->getViewPath($entity);
+ }
+ }
+}
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php b/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php
new file mode 100644
index 0000000..ae5528f
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Tests/UserTranslationUITest.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\Tests\UserTranslationUITest.
+ */
+
+namespace Drupal\user\Tests;
+
+use Drupal\translation_entity\Tests\EntityTranslationUITest;
+
+/**
+ * Tests the User Translation UI.
+ */
+class UserTranslationUITest extends EntityTranslationUITest {
+
+ /**
+ * The user name of the test user.
+ */
+ protected $name;
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('language', 'translation_entity', 'user');
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'User translation UI',
+ 'description' => 'Tests the user translation UI.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Overrides \Drupal\simpletest\WebTestBase::setUp().
+ */
+ function setUp() {
+ $this->entityType = 'user';
+ $this->testLanguageSelector = FALSE;
+ $this->name = $this->randomName();
+ parent::setUp();
+ }
+
+ /**
+ * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getTranslatorPermission().
+ */
+ function getTranslatorPermissions() {
+ return array('administer users', "translate $this->entityType entities", 'edit original values');
+ }
+
+ /**
+ * Overrides \Drupal\translation_entity\Tests\EntityTranslationUITest::getNewEntityValues().
+ */
+ protected function getNewEntityValues($langcode) {
+ // User name is not translatable hence we use a fixed value.
+ return array('name' => $this->name) + parent::getNewEntityValues($langcode);
+ }
+
+}
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index 095a6e2..f4363cf 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -307,6 +307,17 @@ function user_admin_settings($form, &$form_state) {
'#description' => t('This role will be automatically assigned new permissions whenever a module is enabled. Changing this setting will not affect existing permissions.'),
);
+ // @todo Remove this check once language settings are generalized.
+ if (module_exists('translation_entity')) {
+ $form['language'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Language settings'),
+ '#tree' => TRUE,
+ );
+ $form_state['translation_entity']['key'] = 'language';
+ $form['language'] += translation_entity_enable_widget('user', 'user', $form, $form_state);
+ }
+
// User registration settings.
$form['registration_cancellation'] = array(
'#type' => 'fieldset',
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 700ea7a..6911853 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1401,6 +1401,8 @@ function user_admin_paths() {
'user/*/cancel' => TRUE,
'user/*/edit' => TRUE,
'user/*/edit/*' => TRUE,
+ 'user/*/translations' => TRUE,
+ 'user/*/translations/*' => TRUE,
);
return $paths;
}