diff --git a/core/includes/entity.api.php b/core/includes/entity.api.php
index e9fb855..fb74eaa 100644
--- a/core/includes/entity.api.php
+++ b/core/includes/entity.api.php
@@ -141,6 +141,20 @@
  *       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.
+ *   - 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.
  *
  * @see entity_load()
  * @see entity_load_multiple()
diff --git a/core/includes/entity.inc b/core/includes/entity.inc
index 48ee728..c908ed1 100644
--- a/core/includes/entity.inc
+++ b/core/includes/entity.inc
@@ -107,6 +107,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
@@ -495,7 +510,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 8f462de..d9aa007 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -275,37 +275,43 @@ public function getTranslation($langcode, $strict = TRUE) {
   /**
    * Returns the languages the entity is translated to.
    *
-   * @todo: Remove once all entity types implement the entity field API. This
-   * is deprecated by
-   * TranslatableInterface::getTranslationLanguages().
+   * @todo: Remove once all entity types implement the entity field API.
+   * This is deprecated by
+   * Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages().
    */
   public function translations() {
-    $languages = array();
+    return $this->getTranslationLanguages(FALSE);
+  }
+
+  /**
+   * Implements TranslatableInterface::getTranslationLanguages().
+   */
+  public function getTranslationLanguages($include_default = TRUE) {
+    // @todo: Replace by EntityNG implementation once all entity types have been
+    // converted to use the entity field API.
+    $default_language = $this->language();
+    $languages = array($default_language->langcode => $default_language);
     $entity_info = $this->entityInfo();
-    if ($entity_info['fieldable'] && ($default_language = $this->language())) {
+
+    if ($entity_info['fieldable']) {
       // Go through translatable properties and determine all languages for
       // which translated values are available.
       foreach (field_info_instances($this->entityType, $this->bundle()) as $field_name => $instance) {
         $field = field_info_field($field_name);
         if (field_is_translatable($this->entityType, $field) && isset($this->$field_name)) {
-          foreach ($this->$field_name as $langcode => $value)  {
+          foreach (array_filter($this->$field_name) as $langcode => $value)  {
             $languages[$langcode] = TRUE;
           }
         }
       }
-      // Remove the default language from the translations.
+      $languages = array_intersect_key(language_list(LANGUAGE_ALL), $languages);
+    }
+
+    if (empty($include_default)) {
       unset($languages[$default_language->langcode]);
-      $languages = array_intersect_key(language_list(), $languages);
     }
-    return $languages;
-  }
 
-  /**
-   * Implements TranslatableInterface::getTranslationLanguages().
-   */
-  public function getTranslationLanguages($include_default = TRUE) {
-    // @todo: Replace by EntityNG implementation once all entity types have been
-    // converted to use the entity field API.
+    return $languages;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Entity/EntityFormController.php b/core/lib/Drupal/Core/Entity/EntityFormController.php
index f75015f..a6507a1 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormController.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormController.php
@@ -178,6 +178,7 @@ public function validate(array $form, array &$form_state) {
    *   A reference to a keyed array containing the current state of the form.
    */
   public function submit(array $form, array &$form_state) {
+    $this->submitEntityLanguage($form, $form_state);
     $entity = $this->buildEntity($form, $form_state);
     $this->setEntity($entity, $form_state);
     return $entity;
@@ -212,7 +213,7 @@ public function delete(array $form, array &$form_state) {
    */
   public function getFormLangcode(array $form_state) {
     $entity = $this->getEntity($form_state);
-    $translations = $entity->translations();
+    $translations = $entity->getTranslationLanguages();
 
     if (!empty($form_state['langcode'])) {
       $langcode = $form_state['langcode'];
@@ -234,6 +235,49 @@ public function getFormLangcode(array $form_state) {
   }
 
   /**
+   * Implements EntityFormControllerInterface::isDefaultFormLangcode().
+   */
+  public function isDefaultFormLangcode($form_state) {
+    return $this->getFormLangcode($form_state) == $this->getEntity($form_state)->language()->langcode;
+  }
+
+  /**
+   * Handle possible entity language changes.
+   */
+  protected function submitEntityLanguage(array $form, array &$form_state) {
+    // Update the form language as it might have changed.
+    if (isset($form_state['values']['langcode']) && $this->isDefaultFormLangcode($form_state)) {
+      $form_state['langcode'] = $form_state['values']['langcode'];
+    }
+
+    $entity = $this->getEntity($form_state);
+    $entity_type = $entity->entityType();
+
+    if (field_has_translation_handler($entity_type)) {
+      $form_langcode = $this->getFormLangcode($form_state);
+
+      // If we are editing the default language values, we use the submitted
+      // entity language as the new language for fields to handle any language
+      // change. Otherwise the current form language is the proper value, since
+      // in this case it is not supposed to change.
+      $current_langcode = $entity->language()->langcode == $form_langcode ? $form_state['values']['langcode'] : $form_langcode;
+
+      foreach (field_info_instances($entity_type, $entity->bundle()) as $instance) {
+        $field_name = $instance['field_name'];
+        $field = field_info_field($field_name);
+        $previous_langcode = $form[$field_name]['#language'];
+
+        // Handle a possible language change: new language values are inserted,
+        // previous ones are deleted.
+        if ($field['translatable'] && $previous_langcode != $current_langcode) {
+          $form_state['values'][$field_name][$current_langcode] = $form_state['values'][$field_name][$previous_langcode];
+          $form_state['values'][$field_name][$previous_langcode] = array();
+        }
+      }
+    }
+  }
+
+  /**
    * Implements Drupal\Core\Entity\EntityFormControllerInterface::buildEntity().
    */
   public function buildEntity(array $form, array &$form_state) {
diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php
index 26a227d..2238fc3 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormControllerInterface.php
@@ -44,6 +44,14 @@ public function build(array $form, array &$form_state, EntityInterface $entity);
   public function getFormLangcode(array $form_state);
 
   /**
+   * Checks whether the current form language matches the entity one.
+   *
+   * @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 @@ public function buildEntity(array $form, array &$form_state) {
     // without changing existing entity properties that are not being edited by
     // this form. Copying field values must be done using field_attach_submit().
     $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $entity->bundle())) : $form_state['values'];
+    $translation = $entity->getTranslation($this->getFormLangcode($form_state), FALSE);
+    $definitions = $translation->getPropertyDefinitions();
     foreach ($values_excluding_fields as $key => $value) {
-      $entity->$key = $value;
+      if (isset($definitions[$key])) {
+        $translation->$key = $value;
+      }
     }
 
-    // Invoke all specified builders for copying form values to entity properties.
+    // Invoke all specified builders for copying form values to entity
+    // properties.
     if (isset($form['#entity_builders'])) {
       foreach ($form['#entity_builders'] as $function) {
-        $function($entity_type, $entity, $form, $form_state);
+        call_user_func_array($function, array($entity_type, $entity, &$form, &$form_state));
       }
     }
 
diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php
index 7520466..707d46e 100644
--- a/core/lib/Drupal/Core/Entity/EntityNG.php
+++ b/core/lib/Drupal/Core/Entity/EntityNG.php
@@ -269,21 +269,13 @@ public function getTranslationLanguages($include_default = TRUE) {
       $translations[$this->language()->langcode] = TRUE;
     }
 
-    // Now get languages based upon translation langcodes.
-    $languages = array_intersect_key(language_list(LANGUAGE_ALL), $translations);
+    // Now get languages based upon translation langcodes. Empty languages must
+    // be filtered out as they concern empty/unset properties.
+    $languages = array_intersect_key(language_list(LANGUAGE_ALL), array_filter($translations));
     return $languages;
   }
 
   /**
-   * Overrides Entity::translations().
-   *
-   * @todo: Remove once Entity::translations() gets removed.
-   */
-  public function translations() {
-    return $this->getTranslationLanguages(FALSE);
-  }
-
-  /**
    * Enables or disable the compatibility mode.
    *
    * @param bool $enabled
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index df0a33a..dfa134e 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -126,6 +126,7 @@ function comment_entity_info() {
           'custom settings' => FALSE,
         ),
       ),
+      'translation controller class' => 'Drupal\comment\CommentTranslationController',
       'static cache' => FALSE,
     ),
   );
@@ -1040,6 +1041,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;
 }
 
@@ -1137,10 +1148,33 @@ 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_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');
+    }
   }
 }
 
 /**
+ * Comment translation settings submit handler.
+ *
+ * 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.
+ */
+function comment_translation_configuration_element_submit($form, &$form_state) {
+  $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 @@ protected function actions(array $form, array &$form_state) {
       ),
     );
 
-    $element['#weight'] = $form['comment_body']['#weight'] + 0.01;
-
     return $element;
   }
 
diff --git a/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php b/core/modules/comment/lib/Drupal/comment/CommentTranslationController.php
new file mode 100644
index 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/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 @@ public function validate(array $form, array &$form_state) {
    * Overrides Drupal\Core\Entity\EntityFormController::submit().
    */
   public function submit(array $form, array &$form_state) {
-    $this->submitNodeLanguage($form, $form_state);
-
     // Build the node object from the submitted values.
     $node = parent::submit($form, $form_state);
 
@@ -337,36 +335,6 @@ public function submit(array $form, array &$form_state) {
   }
 
   /**
-   * Handle possible node language changes.
-   */
-  protected function submitNodeLanguage(array $form, array &$form_state) {
-    if (field_has_translation_handler('node', 'node')) {
-      $bundle = $form_state['values']['type'];
-      $entity = $this->getEntity($form_state);
-      $form_langcode = $this->getFormLangcode($form_state);
-
-      // If we are editing the default language values, we use the submitted
-      // entity language as the new language for fields to handle any language
-      // change. Otherwise the current form language is the proper value, since
-      // in this case it is not supposed to change.
-      $current_langcode = $entity->language()->langcode == $form_langcode ? $form_state['values']['langcode'] : $form_langcode;
-
-      foreach (field_info_instances('node', $bundle) as $instance) {
-        $field_name = $instance['field_name'];
-        $field = field_info_field($field_name);
-        $previous_langcode = $form[$field_name]['#language'];
-
-        // Handle a possible language change: new language values are inserted,
-        // previous ones are deleted.
-        if ($field['translatable'] && $previous_langcode != $current_langcode) {
-          $form_state['values'][$field_name][$current_langcode] = $form_state['values'][$field_name][$previous_langcode];
-          $form_state['values'][$field_name][$previous_langcode] = array();
-        }
-      }
-    }
-  }
-
-  /**
    * Form submission handler for the 'preview' action.
    *
    * @param $form
diff --git a/core/modules/node/lib/Drupal/node/NodeTranslationController.php b/core/modules/node/lib/Drupal/node/NodeTranslationController.php
new file mode 100644
index 0000000..da078ac
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/NodeTranslationController.php
@@ -0,0 +1,50 @@
+<?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/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 55989de..ef0845b 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -232,6 +232,7 @@ function node_entity_info() {
           'custom settings' => FALSE,
         ),
       ),
+      'translation controller class' => 'Drupal\node\NodeTranslationController',
     ),
   );
 
@@ -309,6 +310,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,
     );
@@ -2494,7 +2497,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;
     }
@@ -2514,7 +2517,7 @@ function _node_index_node(Node $node) {
   // results half-life calculation.
   variable_set('node_cron_last', $node->changed);
 
-  $languages = array_merge(array(language_load($node->langcode)), $node->translations());
+  $languages = $node->getTranslationLanguages();
 
   foreach ($languages as $language) {
     // Render the node.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php
index e0b25fb..57c4c37 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityTranslationTest.php
@@ -302,4 +302,5 @@ function testMultilingualProperties() {
     $result = $query->execute();
     $this->assertEqual(count($result), 1, 'One entity loaded by name, uid and field value using different language meta conditions.');
   }
+
 }
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 46def82..0b5f415 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -5,6 +5,9 @@
  * Test module for the entity API providing an entity type for testing.
  */
 
+use Drupal\entity_test\EntityTest;
+
+
 /**
  * Implements hook_entity_info().
  */
@@ -23,11 +26,15 @@ function entity_test_entity_info() {
       'id' => 'id',
       'uuid' => 'uuid',
     ),
+    'menu base path' => 'entity-test/manage/%entity_test',
+    'translation controller class' => 'Drupal\entity_test\EntityTestTranslationController',
   );
+
   // Optionally specify a translation handler for testing translations.
   if (variable_get('entity_test_translation')) {
     $items['entity_test']['translation']['entity_test'] = TRUE;
   }
+
   return $items;
 }
 
@@ -85,8 +92,8 @@ function entity_test_add() {
 /**
  * Menu callback: displays the 'Edit existing entity_test' form.
  */
-function entity_test_edit($entity) {
-  drupal_set_title(t('entity_test @id', array('@id' => $entity->id())), PASS_THROUGH);
+function entity_test_edit(EntityTest $entity) {
+  drupal_set_title($entity->label(), PASS_THROUGH);
   return entity_get_form($entity);
 }
 
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php
index 17e8470..1a19cf4 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTest.php
@@ -55,4 +55,12 @@ public function __construct(array $values, $entity_type) {
     unset($this->name);
     unset($this->user_id);
   }
+
+  /**
+   * Overrides Drupal\entity\Entity::label().
+   */
+  public function label($langcode = LANGUAGE_DEFAULT) {
+    return $this->getTranslation($langcode)->name->value;
+  }
+
 }
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
index e51f474..a65f06f 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
@@ -43,21 +43,14 @@ public function form(array $form, array &$form_state, EntityInterface $entity) {
       '#weight' => -10,
     );
 
-    return $form;
-  }
+    $form['langcode'] = array(
+      '#title' => t('Language'),
+      '#type' => 'language_select',
+      '#default_value' => $entity->language()->langcode,
+      '#languages' => LANGUAGE_ALL,
+    );
 
-  /**
-   * Overrides Drupal\Core\Entity\EntityFormController::submit().
-   */
-  public function submit(array $form, array &$form_state) {
-    $entity = parent::submit($form, $form_state);
-    $langcode = $this->getFormLangcode($form_state);
-    // Updates multilingual properties.
-    $translation = $entity->getTranslation($langcode);
-    foreach (array('name', 'user_id') as $name) {
-      $translation->$name->setValue($form_state['values'][$name]);
-    }
-    return $entity;
+    return $form;
   }
 
   /**
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
index 67e4b9b..41aa9dc 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
@@ -85,6 +85,13 @@ protected function attachPropertyData(&$queried_entities) {
   protected function postSave(EntityInterface $entity, $update) {
     $default_langcode = $entity->language()->langcode;
 
+    // Delete and insert to handle removed values.
+    db_delete('entity_test_property_data')
+      ->condition('id', $entity->id())
+      ->execute();
+
+    $query = db_insert('entity_test_property_data');
+
     foreach ($entity->getTranslationLanguages() as $langcode => $language) {
       $translation = $entity->getTranslation($langcode);
 
@@ -96,12 +103,12 @@ protected function postSave(EntityInterface $entity, $update) {
         'user_id' => $translation->user_id->value,
       );
 
-      db_merge('entity_test_property_data')
-        ->fields($values)
-        ->condition('id', $values['id'])
-        ->condition('langcode', $values['langcode'])
-        ->execute();
+      $query
+        ->fields(array_keys($values))
+        ->values($values);
     }
+
+    $query->execute();
   }
 
   /**
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php
new file mode 100644
index 0000000..e7ca050
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestTranslationController.php
@@ -0,0 +1,28 @@
+<?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/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php
new file mode 100644
index 0000000..a8e6dd7
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermTranslationController.php
@@ -0,0 +1,38 @@
+<?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');
+  }
+
+  /**
+   * Submit handler for the save action.
+   */
+  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/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 @@ protected function actions(array $form, array &$form_state) {
         array_unshift($actions['submit']['#submit'],'language_configuration_element_submit');
         array_unshift($actions['submit']['#submit'], array($this, 'languageConfigurationSubmit'));
       }
+      // We cannot leverage the regular submit handler definition because we
+      // have button-specific ones here. Hence we need to explicitly set it for
+      // the submit action, otherwise it would be ignored.
+      if (module_exists('translation_entity')) {
+        array_unshift($actions['submit']['#submit'], 'translation_entity_language_configuration_element_submit');
+      }
       return $actions;
     }
     else {
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 874c78a..9008440 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -138,8 +138,11 @@ function taxonomy_entity_info() {
           'custom settings' => FALSE,
         ),
       ),
+      'menu base path' => 'taxonomy/term/%taxonomy_term',
+      'translation controller class' => 'Drupal\taxonomy\TermTranslationController',
     ),
   );
+
   foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) {
     $return['taxonomy_term']['bundles'][$machine_name] = array(
       'label' => $vocabulary->name,
@@ -151,6 +154,7 @@ function taxonomy_entity_info() {
       ),
     );
   }
+
   $return['taxonomy_vocabulary'] = array(
     'label' => t('Taxonomy vocabulary'),
     'entity class' => 'Drupal\taxonomy\Vocabulary',
@@ -410,6 +414,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..8dcea24
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationController.php
@@ -0,0 +1,321 @@
+<?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 {
+
+  protected $entityType;
+  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);
+    $no_translations = count($translations) < 2;
+
+    // Adjust page title to specify the current language being edited, if we
+    // have at least one translation.
+    $languages = language_list();
+    if (isset($languages[$form_langcode]) && (!$no_translations || $new_translation)) {
+      $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 (!$no_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,
+        'source' => array(
+          '#type' => 'select',
+          '#default_value' => $source_langcode,
+          '#options' => array(),
+        ),
+        'submit' => array(
+          '#type' => 'submit',
+          '#value' => t('Change'),
+          '#submit' => array(array($this, 'entityFormSourceChange')),
+        ),
+      );
+      foreach (language_list(LANGUAGE_CONFIGURABLE) as $language) {
+        if (isset($translations[$language->langcode])) {
+          $form['source_langcode']['source']['#options'][$language->langcode] = $language->name;
+        }
+      }
+    }
+
+    // Disable languages for existing translations, so it is not possible to
+    // switch this node to some language which is already in the translation
+    // set.
+    $language_widget = isset($form['langcode']) && $form['langcode']['#type'] == 'language_select';
+    if ($language_widget && count($translations) > 1) {
+      $form['langcode']['#options'] = array();
+      foreach (language_list(LANGUAGE_CONFIGURABLE) as $language) {
+        if (empty($translations[$language->langcode]) || $language->langcode == $entity_langcode) {
+          $form['langcode']['#options'][$language->langcode] = $language->name;
+        }
+      }
+    }
+
+    if ($is_translation) {
+      if ($language_widget) {
+        $form['langcode']['#disabled'] = TRUE;
+      }
+
+      // Replace the delete button with the delete translation one.
+      if (!$new_translation) {
+        $weight = 100;
+        foreach (array('delete', 'submit') as $key) {
+          if (isset($form['actions'][$key]['weight'])) {
+            $weight = $form['actions'][$key]['weight'];
+            break;
+          }
+        }
+        $form['actions']['delete_translation'] = array(
+          '#type' => 'submit',
+          '#value' => t('Delete translation'),
+          '#weight' => $weight,
+          '#submit' => array(array($this, 'entityFormDeleteTranslation')),
+        );
+      }
+
+      // Always remove the delete button on translation forms.
+      unset($form['actions']['delete']);
+    }
+
+    // We need to display the translation tab only when there is at least one
+    // translation available or a new one is about to be created.
+    if ($new_translation || count($translations) > 1) {
+      $form['translation'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Translation'),
+        '#collapsible' => TRUE,
+        '#collapsed' => TRUE,
+        '#tree' => TRUE,
+        '#weight' => 10,
+        '#access' => $this->getTranslationAccess($entity, $form_langcode),
+      );
+
+      $translate = !$new_translation && $entity->retranslate[$form_langcode];
+      if (!$translate) {
+        $form['translation']['retranslate'] = array(
+          '#type' => 'checkbox',
+          '#title' => t('Flag 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 because the source content has changed. Uncheck when the translation is up to date again.'),
+        );
+      }
+    }
+
+    // Process the submitted values before they are stored.
+    $form['#entity_builders'][] = array($this, 'entityFormEntityBuild');
+
+    // Handle entity deletion.
+    if (isset($form['actions']['delete'])) {
+      $form['actions']['delete']['#submit'][] = array($this, 'entityFormDelete');
+    }
+  }
+
+  /**
+   * Entity builder method.
+   */
+  public function entityFormEntityBuild($entity_type, $entity, $form, &$form_state) {
+    $form_controller = translation_entity_form_controller($form_state);
+    $form_langcode = $form_controller->getFormLangcode($form_state);
+    $source_langcode = $this->getSourceLangcode($form_state);
+
+    if ($source_langcode) {
+      // @todo Use the entity setter when all entities support multilingual
+      // properties.
+      $entity->source[$form_langcode] = $source_langcode;
+    }
+
+    // Ensure every key has at least a default value. Subclasses may provide
+    // entity-specific values to alter them.
+    $values = isset($form_state['values']['translation']) ? $form_state['values']['translation'] : array();
+    $entity->retranslate[$form_langcode] = isset($values['translate']) && $values['translate'];
+
+    if (!empty($values['retranslate'])) {
+      $this->retranslate($entity, $form_langcode);
+    }
+  }
+
+  /**
+   * Submit handler for the source language change.
+   */
+  public function entityFormSourceChange($form, &$form_state) {
+    $form_controller = translation_entity_form_controller($form_state);
+    $entity = $form_controller->getEntity($form_state);
+    $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)));
+  }
+
+  /**
+   * Submit handler for the entity deletion.
+   */
+  function entityFormDelete($form, &$form_state) {
+    $form_controller = translation_entity_form_controller($form_state);
+    $entity = $form_controller->getEntity($form_state);
+    if (count($entity->getTranslationLanguages()) > 1) {
+      drupal_set_message(t('This will delete all the translations of %label.', array('%label' => $entity->label())), 'warning');
+    }
+  }
+
+  /**
+   * Submit handler for the entity translation deletion.
+   */
+  function entityFormDeleteTranslation($form, &$form_state) {
+    $form_controller = translation_entity_form_controller($form_state);
+    $entity = $form_controller->getEntity($form_state);
+    $base_path = $this->getBasePath($entity);
+    $form_langcode = $form_controller->getFormLangcode($form_state);
+    $form_state['redirect'] = $base_path . '/translations/delete/' . $form_langcode;
+  }
+
+  /**
+   * Returns the title to be used for the entity form page.
+   */
+  protected function entityFormTitle(EntityInterface $entity) {
+    return $entity->label();
+  }
+
+  /**
+   * Returns an instance of the given path.
+   *
+   * @param $path
+   *   An internal path containing the entity id wildcard.
+   *
+   * @return
+   *   The instantiated path.
+   */
+  protected function getPathInstance($path, $entity_id) {
+    $wildcard = $this->entityInfo['menu path wildcard'];
+    return str_replace($wildcard, $entity_id, $path);
+  }
+}
diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php
new file mode 100644
index 0000000..8b68970
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/EntityTranslationControllerInterface.php
@@ -0,0 +1,191 @@
+<?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' key 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() {
+ *     $info['myentity'] = array(
+ *       // ...
+ *       'menu base path' => 'mymodule/myentity/%my_entity_loader',
+ *       'menu path widlcard' => '%my_entity_loader',
+ *       'translation controller class' => 'Drupal\mymodule\MyEntityTranslationController',
+ *       'translation' => array(
+ *         'translation_entity' => array(
+ *           'access callback' => 'mymodule_myentity_translate_access',
+ *           'access arguments' => array(2),
+ *         ),
+ *       ),
+ *     );
+ *     return $info;
+ *   }
+ * @endcode
+ */
+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/EntityTranslationUITest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php
new file mode 100644
index 0000000..1ef0429
--- /dev/null
+++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationUITest.php
@@ -0,0 +1,190 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\entity\Tests\EntityTranslationUITest.
+ */
+
+namespace Drupal\translation_entity\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the Entity Translation UI.
+ */
+class EntityTranslationUITest extends WebTestBase {
+
+  /**
+   * The enabled languages.
+   *
+   * @var array
+   */
+  protected $langcodes;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('language', 'translation_entity', 'entity_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Translation UI',
+      'description' => 'Tests the basic entity translation UI.',
+      'group' => 'Entity Translation UI',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
+    $this->drupalLogin($admin_user);
+
+    $languages = array('it' => 'Italian', 'fr' => 'French');
+    $this->langcodes = array_keys($languages);
+    array_unshift($this->langcodes, language_default()->langcode);
+
+    // Add predefined language.
+    foreach ($languages as $langcode => $name) {
+      $edit = array('predefined_langcode' => $langcode);
+      $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+      $this->assertText($name, 'Language added successfully.');
+    }
+
+    // Enable translation for the entity_test entity type and ensure the change
+    // is picked up.
+    translation_entity_set_config('entity_test', 'entity_test', 'enabled', TRUE);
+    drupal_static_reset();
+    entity_info_cache_clear();
+    menu_router_rebuild();
+
+    $translator = $this->drupalCreateUser(array('administer entity_test content', 'translate entity_test entities', 'edit original values'));
+    $this->drupalLogin($translator);
+  }
+
+  /**
+   * Tests the basic translation UI.
+   */
+  function testTranslationUI() {
+    // Make the test field translatable.
+    $field = field_info_field('field_test_text');
+    $field['translatable'] = TRUE;
+    field_update_field($field);
+
+    // Create a new test entity with original values in the default language.
+    $default_langcode = language_default()->langcode;
+
+    $values[$default_langcode] = array(
+      'langcode' => $default_langcode,
+      'name' => $this->randomName(8),
+      'user_id' => mt_rand(0, 128),
+      'field_test_text' => $this->randomName(16),
+    );
+
+    $this->drupalPost('entity-test/add', $this->getEditValues($values, $default_langcode, TRUE), t('Save'));
+    $entity = $this->loadEntityByName($values[$default_langcode]['name']);
+    $this->assertTrue($entity, t('Entity found in the database.'));
+
+    $translation = $entity->getTranslation($default_langcode);
+    foreach ($values[$default_langcode] as $property => $value) {
+      if ($property != 'langcode') {
+        // @todo Remove this workaround when field default language is correctly
+        // handled.
+        $stored_value = $property != 'field_test_text' ? $translation->get($property)->value : $entity->values['field_test_text'][$default_langcode][0]['value'];
+        $message = format_string('@property correctly stored in the default language.', array('@property' => $property));
+        $this->assertEqual($stored_value, $value, $message);
+      }
+    }
+
+    // Add an entity translation.
+    $langcode = 'it';
+    $values[$langcode] = array(
+      'name' => $this->randomName(8),
+      'user_id' => mt_rand(0, 128),
+      'field_test_text' => $this->randomName(16),
+    );
+
+    $this->drupalPost($langcode . '/entity-test/manage/' . $entity->id() . '/translations/add/' . $default_langcode . '/' . $langcode, $this->getEditValues($values, $langcode), t('Save'));
+    $this->assertFieldByXPath('//select[@id="edit-langcode" and @disabled]', $default_langcode, 'Language selector correclty disabled on translations.');
+    $entity = entity_test_load($entity->id(), TRUE);
+
+    // Switch the source language.
+    $langcode = 'fr';
+    $source_langcode = 'it';
+    $edit = array('source_langcode[source]' => $source_langcode);
+    $this->drupalPost($langcode . '/entity-test/manage/' . $entity->id() . '/translations/add/' . $default_langcode . '/' . $langcode, $edit, t('Change'));
+    $this->assertFieldByXPath('//input[@name="field_test_text[fr][0][value]"]', $values[$source_langcode]['field_test_text'], 'Source language correctly switched.');
+
+    // Add another translation and mark the other ones as outdated.
+    $values[$langcode] = array(
+      'name' => $this->randomName(8),
+      'user_id' => mt_rand(0, 128),
+      'field_test_text' => $this->randomName(16),
+    );
+    $edit = $this->getEditValues($values, $langcode) + array('translation[retranslate]' => TRUE);
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $entity = entity_test_load($entity->id(), TRUE);
+
+    // Check that the entered values have been correctly stored.
+    foreach ($values as $langcode => $property_values) {
+      $translation = $entity->getTranslation($langcode);
+      foreach ($property_values as $property => $value) {
+        if ($property != 'langcode') {
+          $stored_value = $translation->get($property);
+          // @todo Remove this workaround when field default language is correctly
+          // handled.
+          $stored_value = $property != 'field_test_text' ? $translation->get($property)->value : $entity->values['field_test_text'][$langcode][0]['value'];
+          $message = format_string('%property correctly stored with language %language.', array('%property' => $property, '%language' => $langcode));
+          $this->assertEqual($stored_value, $value, $message);
+        }
+      }
+    }
+
+    // Check that every translation has the correct "outdated" status.
+    foreach ($this->langcodes as $enabled_langcode) {
+      $prefix = $enabled_langcode != $default_langcode ? $enabled_langcode . '/' : '';
+      $this->drupalGet($prefix . 'entity-test/manage/' . $entity->id() . '/edit');
+      if ($enabled_langcode == $langcode) {
+        $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is not checked by default.');
+      }
+      else {
+        $this->assertFieldByXPath('//input[@name="translation[translate]"]', TRUE, 'The translate flag is checked by default.');
+        $edit = array('translation[translate]' => FALSE);
+        $this->drupalPost(NULL, $edit, t('Save'));
+        $this->assertFieldByXPath('//input[@name="translation[retranslate]"]', FALSE, 'The retranslate flag is now shown.');
+        $entity = entity_test_load($entity->id(), TRUE);
+        $this->assertFalse($entity->retranslate[$enabled_langcode], 'The "outdated" status has been correctly stored.');
+      }
+    }
+
+    // Confirm and delete a translation.
+    $this->drupalPost(NULL, array(), t('Delete translation'));
+    $this->drupalPost(NULL, array(), t('Delete'));
+    $entity = entity_test_load($entity->id(), TRUE);
+    $translations = $entity->getTranslationLanguages();
+    $this->assertTrue(count($translations) == 2 && empty($translations[$enabled_langcode]), 'Translation successfully deleted.');
+  }
+
+  /**
+   * Returns an edit array containing the values to be posted.
+   */
+  protected function getEditValues($values, $langcode, $new = FALSE) {
+    $edit = $values[$langcode];
+    $langcode = $new ? LANGUAGE_NOT_SPECIFIED : $langcode;
+    $edit["field_test_text[$langcode][0][value]"] = $edit['field_test_text'];
+    unset($edit['field_test_text']);
+    return $edit;
+  }
+
+  /**
+   * Loads a test entity by name.
+   *
+   * @return Drupal\entity_test\EntityTest
+   *   A test entity matching the given name.
+   */
+  protected function loadEntityByName($name) {
+    return current(entity_load_multiple_by_properties('entity_test', array('name' => $name)));
+  }
+}
diff --git a/core/modules/translation_entity/translation_entity.admin.inc b/core/modules/translation_entity/translation_entity.admin.inc
new file mode 100644
index 0000000..7b44892
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.admin.inc
@@ -0,0 +1,226 @@
+<?php
+
+/**
+ * @file
+ * The entity translation administration forms.
+ */
+
+use Drupal\Core\Entity\EntityFieldQuery;
+
+/**
+ * Returns the confirmation form for changing field translatability.
+ */
+function translation_entity_translatable_form($form, &$form_state, $field_name) {
+  $field = field_info_field($field_name);
+  $t_args = array('%name' => $field_name);
+
+  $warning = t('By submitting this form you will trigger a batch operation.');
+  if ($field['translatable']) {
+    $title = t('Are you sure you want to disable translation for the %name field?', $t_args);
+    $warning .= "<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);
+}
+
+/**
+ * Submit handler for the field settings form.
+ *
+ * This submit handler maintains consistency between the translatability of an
+ * entity and the language under which the field data is stored. When a field is
+ * marked as translatable, all the data in
+ * $entity->{field_name}[LANGUAGE_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($form, $form_state) {
+  // This is the current state that we want to reverse.
+  $translatable = $form_state['values']['translatable'];
+  $field_name = $form_state['field']['field_name'];
+  $field = field_info_field($field_name);
+
+  if ($field['translatable'] !== $translatable) {
+    // Field translatability has changed since form creation, abort.
+    $t_args = array('%field_name');
+    $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.
+ */
+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);
+}
+
+
+/**
+ * Converts field data to or from LANGUAGE_NOT_SPECIFIED.
+ */
+function translation_entity_translatable_batch($translatable, $field_name, &$context) {
+  if (empty($context['sandbox'])) {
+    $context['sandbox']['progress'] = 0;
+
+    // How many entities will need processing?
+    $query = new EntityFieldQuery();
+    $count = $query
+      ->fieldCondition($field_name)
+      ->count()
+      ->execute();
+
+    if (intval($count) === 0) {
+      // Nothing to do.
+      $context['finished'] = 1;
+      return;
+    }
+    $context['sandbox']['max'] = $count;
+  }
+
+  // Number of entities to be processed for each step.
+  $limit = 10;
+  $offset = $context['sandbox']['progress'];
+  $query = new EntityFieldQuery();
+  $result = $query
+    ->fieldCondition($field_name)
+    ->entityOrderBy('entity_id')
+    ->range($offset, $limit)
+    ->execute();
+
+  foreach ($result as $entity_type => $entities) {
+    foreach (entity_load_multiple($entity_type, array_keys($entities)) as $id => $entity) {
+      $context['sandbox']['progress']++;
+      $langcode = $entity->language()->langcode;
+
+      // Skip process for language neutral entities.
+      if ($langcode == LANGUAGE_NOT_SPECIFIED) {
+        continue;
+      }
+
+      // We need a two-steps approach while updating field translations: given
+      // that field-specific update functions might rely on the stored values to
+      // perform their processing, see for instance file_field_update(), first
+      // we need to store the new translations and only after we can remove the
+      // old ones. Otherwise we might have data loss, since the removal of the
+      // old translations might occur before the new ones are stored.
+      if ($translatable && isset($entity->{$field_name}[LANGUAGE_NOT_SPECIFIED])) {
+        // If the field is being switched to translatable and has data for
+        // LANGUAGE_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, $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);
+  }
+}
+
+/**
+ * Checks the exit status of the batch operation.
+ */
+function translation_entity_translatable_batch_done($success, $results, $operations) {
+  if ($success) {
+    drupal_set_message(t("Data successfully processed."));
+  }
+  else {
+    // @todo: Do something about this case.
+    drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields."));
+  }
+}
+
diff --git a/core/modules/translation_entity/translation_entity.info b/core/modules/translation_entity/translation_entity.info
new file mode 100644
index 0000000..4a28def
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.info
@@ -0,0 +1,6 @@
+name = Entity Translation
+description =  Allows entities to be translated into different languages.
+dependencies[] = language
+package = Core
+version = VERSION
+core = 8.x
diff --git a/core/modules/translation_entity/translation_entity.install b/core/modules/translation_entity/translation_entity.install
new file mode 100644
index 0000000..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..0e74893
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.module
@@ -0,0 +1,618 @@
+<?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;
+
+
+/**
+ * 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 .= '<p>' . t('To be able to create and manage translations you need to <a href="!url">enable at least two languages</a> and enable translation for any element whose content you wish to translate.', array('!url' => url('admin/config/regional/language'))) . '</p>';
+      $output .= '<dl>';
+      $output .= '<dt>' . t('Enabling translation') . '</dt>';
+      $output .= '<dd>' . t('To enable translation for a certain <em>Content type</em> go to the related administration page, locate the Language settings, uncheck <em>Hide language selector</em> and check <em>Enable translation</em>. To enable translation for <em>Comments</em> just switch to the Comment settings in the same page. You can configure <em>Taxonomy terms</em> the samy way by accessing the Vocabulary administration pages in the Taxonomy section. Similarly <em>User accounts</em> can be enabled for translations in the <a href="!url">Account settings</a> page.', array('!url' => 'admin/config/people/accounts'));
+      $output .= '<p>' . t('If you need to specify more precisely what should be translated, you can configure it at the field level by changing the <em>Global field settings</em> in the related administration pages.') . '</p></dd>';
+      $output .= '<dt>' . t('Translating content') . '</dt>';
+      $output .= '<dd>' . t('After enabling a site for translation you can create a new content of the configured type or edit an existing one and assign it a language. Once this is done you will see a <em>Translations</em> item that will give you access to an overview of the translation status for the current content. From here you can create new translations and editing or deleting existing ones. This process is similar for every translatable element on your site, such as taxonomy terms, comments or user accounts.') . '</dd>';
+      $output .= '<dt>' . t('Source language') . '</dt>';
+      $output .= '<dd>' . t('If you enable more than two languages and you create at least one content translation, when creating subsequent translations you will be able to choose which language translate from through the <em>Source language</em> selector. If you choose a different language from the default one, all the translation form elements will be populated with values in the specified language.') . '</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. Individual translations may also be marked for revision by selecting the <em>This translation needs to be updated</em> check box on the translation editing form.') . '</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(&$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');
+
+    if (translation_entity_enabled($entity_type, NULL, TRUE)) {
+      // 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'));
+      $item = array_intersect_key($info['translation']['translation_entity'], $keys) + array('file' => 'translation_entity.pages.inc');
+
+      $items["$path/translations"] = array(
+        'title' => 'Translations',
+        'page callback' => 'translation_entity_overview',
+        'page arguments' => array($entity_position),
+        'type' => MENU_LOCAL_TASK,
+        'weight' => 2,
+      ) + $item;
+
+      // Add translation callback.
+      // @todo Add the access callback instead of replacing it as soon as the
+      // routing system supports multiple callbacks.
+      $add_path = "$path/translations/add/%language/%language";
+      $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(&$items) {
+  // Some menu loaders in the item paths might have been altered: we need to
+  // replace any menu loader with a plain % to check if base paths are still
+  // compatible.
+  $paths = array();
+  $regex = '|%[^/]+|';
+  foreach ($items as $path => $item) {
+    $path = preg_replace($regex, '%', $path);
+    $paths[$path] = $path;
+  }
+
+  // Check that the declared menu base paths are actually valid.
+  foreach (entity_get_info() as $entity_type => $info) {
+    if (translation_entity_enabled($entity_type)) {
+      $path = $info['menu base path'];
+
+      // If the base path is not defined or is not compatible with any defined
+      // one we cannot provide the translation UI for this entity type.
+      if (!isset($paths[preg_replace($regex, '%', $path)])) {
+        drupal_set_message(t('The entities of type %entity_type do not define a valid base path: it will not be possible to translate them.', array('%entity_type' => $info['label'])), 'warning');
+        unset(
+          $items["$path/translations"],
+          $items["$path/translations/add/%language"],
+          $items["$path/translations/delete/%language"]
+        );
+      }
+      else {
+        $entity_position = count(explode('/', $path)) - 1;
+        $edit_path = $info['menu edit path'];
+
+        if (isset($items[$edit_path])) {
+          // If the edit path is a default local task we need to find the parent
+          // item.
+          $edit_path_split = explode('/', $edit_path);
+          do {
+            $entity_form_item = &$items[implode('/', $edit_path_split)];
+            array_pop($edit_path_split);
+          }
+          while (!empty($entity_form_item['type']) && $entity_form_item['type'] == MENU_DEFAULT_LOCAL_TASK);
+
+          // Make the "Translate" tab follow the "Edit" one when possibile.
+          if (isset($entity_form_item['weight'])) {
+            $items["$path/translations"]['weight'] = $entity_form_item['weight'] + 0.01;
+          }
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Access callback.
+ */
+function translation_entity_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.
+ */
+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 refer to.
+ * @param string $bundle
+ *   The bundle of the entity the setting refer 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(&$form, &$form_state) {
+  if (($form_controller = translation_entity_form_controller($form_state)) && ($entity = $form_controller->getEntity($form_state)) && !$entity->isNew()) {
+    $controller = translation_entity_controller($entity->entityType());
+    $controller->entityFormAlter($form, $form_state, $entity);
+  }
+}
+
+/**
+ * Implements hook_entity_load().
+ */
+function translation_entity_entity_load(array $entities, $entity_type) {
+  $enabled_entities = array();
+
+  if (translation_entity_enabled($entity_type)) {
+    foreach ($entities as $entity) {
+      if (translation_entity_enabled($entity_type, $entity->bundle())) {
+        $enabled_entities[$entity->id()] = $entity;
+      }
+    }
+  }
+
+  if (!empty($enabled_entities)) {
+    translation_entity_load_translation_data($enabled_entities, $entity_type);
+  }
+}
+
+/**
+ * Loads translation data into the given entities.
+ *
+ * @param array $entities
+ *   The entities keyed by entity ID.
+ * @param string $entity_type
+ *   The type of the entities.
+ */
+function translation_entity_load_translation_data(array $entities, $entity_type) {
+  $result = db_select('translation_entity', 'te')
+    ->fields('te', array())
+    ->condition('te.entity_type', $entity_type)
+    ->condition('te.entity_id', array_keys($entities))
+    ->execute();
+
+  foreach ($result as $record) {
+    $entity = $entities[$record->entity_id];
+    // @todo Declare these as entity (translation?) properties.
+    $entity->source[$record->langcode] = $record->source;
+    // @todo Rename to 'translate' when the column is removed from the node
+    // schema.
+    $entity->retranslate[$record->langcode] = (boolean) $record->translate;
+  }
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function translation_entity_entity_insert(EntityInterface $entity) {
+  $entity_type = $entity->entityType();
+  $id = $entity->id();
+  $query = db_insert('translation_entity')
+    ->fields(array('entity_type', 'entity_id', 'langcode', 'source', 'translate'));
+
+  foreach ($entity->getTranslationLanguages() as $langcode => $language) {
+    // @todo Declare these as entity (translation?) properties.
+    $source = (isset($entity->source[$langcode]) ? $entity->source[$langcode] : NULL) . '';
+    $retranslate = intval(!empty($entity->retranslate[$langcode]));
+    $query->values(array($entity_type, $id, $langcode, $source, $retranslate));
+  }
+
+  $query->execute();
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function translation_entity_entity_delete(EntityInterface $entity) {
+  db_delete('translation_entity')
+    ->condition('entity_type', $entity->entityType())
+    ->condition('entity_id', $entity->id())
+    ->execute();
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function translation_entity_entity_update(EntityInterface $entity) {
+  // Delete and create to ensure no stale value remains behind.
+  translation_entity_entity_delete($entity);
+  translation_entity_entity_insert($entity);
+}
+
+/**
+ * Implements hook_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(&$form, &$form_state, $form_id) {
+  $field = $form['#field'];
+  $field_name = $field['field_name'];
+  $translatable = $field['translatable'];
+  $label = t('Field translation');
+  $title = t('Users may translate this field.');
+
+  if (field_has_data($field)) {
+    $path = "admin/config/regional/translation_entity/translatable/$field_name";
+    $status = $translatable ? $title : t('This field is shared among the entity translations.');
+    $link_title = !$translatable ? t('Enable translation') : t('Disable translation');
+
+    $form['field']['translatable'] = array(
+      '#prefix' => '<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.
+ */
+function translation_entity_enable_widget($entity_type, $bundle, &$form, &$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 handler for the language_configuration form element.
+ */
+function translation_entity_language_configuration_element_process($element, &$form_state, &$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;
+}
+
+/**
+ * Checks if translation can be enabled.
+ *
+ * If language is set to one of the special languages and language selector is
+ * not hidden, translation cannot be enabled.
+ */
+function translation_entity_language_configuration_element_validate($element, &$form_state, $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))));
+  }
+}
+
+/**
+ * Stores the entity translation settings.
+ */
+function translation_entity_language_configuration_element_submit($form, &$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..21dc11e
--- /dev/null
+++ b/core/modules/translation_entity/translation_entity.pages.inc
@@ -0,0 +1,249 @@
+<?php
+
+/**
+ * @file
+ * The entity translation user interface.
+ */
+
+use Drupal\Core\Language\Language;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityNG;
+use Drupal\translation_entity\EntityTranslationController;
+
+/**
+ * Translations overview page callback.
+ */
+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) {
+            $path = _field_ui_bundle_admin_path($entity->entityType(), $entity->bundle());
+            $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.
+ */
+function _translation_entity_get_switch_links($path) {
+  $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path);
+  if (empty($links)) {
+    // If content language is set up to fall back to the interface language,
+    // then there will be no switch links for LANGUAGE_TYPE_CONTENT, ergo we
+    // also need to use interface switch links.
+    $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_INTERFACE, $path);
+  }
+  return $links;
+}
+
+/**
+ * Page callback.
+ */
+function translation_entity_add_page(EntityInterface $entity, Language $source = NULL, Language $target = NULL) {
+  $source = !empty($source) ? $source : $entity->language();
+  $target = !empty($target) ? $target : language(LANGUAGE_TYPE_CONTENT);
+  // @todo Exploit the upcoming hook_entity_prepare() when available.
+  translation_entity_prepare_translation($entity, $source, $target);
+  $info = $entity->entityInfo();
+  $operation = isset($info['default operation']) ? $info['default operation'] : 'default';
+  $form_state = entity_form_state_defaults($entity, $operation, $target->langcode);
+  $form_state['translation_entity']['source'] = $source;
+  $form_state['translation_entity']['target'] = $target;
+  $form_id = entity_form_id($entity);
+  return drupal_build_form($form_id, $form_state);
+}
+
+/**
+ * Populates target values with the source values.
+ *
+ * @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) {
+      if (!isset($instances[$property_name])) {
+        // @todo Actually retrieving the property value should not be necessary.
+        $target_translation->$property_name = $source_translation->$property_name->value;
+      }
+      else {
+        $entity->setCompatibilityMode(TRUE);
+        $value = $entity->$property_name;
+        $value[$target->langcode] = $value[$source->langcode];
+        $entity->$property_name = $value;
+        $entity->setCompatibilityMode(FALSE);
+      }
+    }
+  }
+  else {
+    foreach ($instances as $field_name => $instance) {
+      $field = field_info_field($field_name);
+      if (!empty($field['translatable'])) {
+        $value = $entity->get($field_name);
+        $value[$target->langcode] = $value[$source->langcode];
+        $entity->set($field_name, $value);
+      }
+    }
+  }
+}
+
+/**
+ * Translation deletion confirmation form.
+ */
+function translation_entity_delete_confirm(array $form, array $form_state, EntityInterface $entity, Language $language) {
+  $langcode = $language->langcode;
+  $controller = translation_entity_controller($entity->entityType());
+
+  return confirm_form(
+    $form,
+    t('Are you sure you want to delete the @language translation of %label?', array('@language' => $language->name, '%label' => $entity->label())),
+    $controller->getEditPath($entity),
+    t('This action cannot be undone.'),
+    t('Delete'),
+    t('Cancel')
+  );
+}
+
+/**
+ * Submit handler for the translation deletion confirmation.
+ */
+function translation_entity_delete_confirm_submit($form, &$form_state) {
+  list($entity, $language) = $form_state['build_info']['args'];
+  $controller = translation_entity_controller($entity->entityType());
+
+  // Remove the translated values.
+  $controller->removeTranslation($entity, $language->langcode);
+  $entity->save();
+
+  // Remove any existing path alias for the removed translation.
+  if (module_exists('path')) {
+    path_delete(array('source' => $controller->getViewPath($entity), 'langcode' => $language->langcode));
+  }
+
+  $form_state['redirect'] = $controller->getBasePath($entity) . '/translations';
+}
diff --git a/core/modules/user/lib/Drupal/user/ProfileTranslationController.php b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php
new file mode 100644
index 0000000..f10d58e
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/ProfileTranslationController.php
@@ -0,0 +1,38 @@
+<?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');
+  }
+
+  /**
+   * Submit handler for the save action.
+   */
+  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/user/user.admin.inc b/core/modules/user/user.admin.inc
index 396f4c0..327d38d 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 570a199..659317f 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -150,6 +150,9 @@ function user_entity_info() {
         'profile' => 'Drupal\user\ProfileFormController',
         'register' => 'Drupal\user\RegisterFormController',
       ),
+      // @todo Remove this once the profile form controller is associated to the
+      // default operation.
+      'default operation' => 'profile',
       'base table' => 'users',
       'uri callback' => 'user_uri',
       'label callback' => 'user_label',
@@ -175,6 +178,7 @@ function user_entity_info() {
           'custom settings' => FALSE,
         ),
       ),
+      'translation controller class' => 'Drupal\user\ProfileTranslationController',
     ),
   );
 }
@@ -1442,6 +1446,8 @@ function user_admin_paths() {
     'user/*/cancel' => TRUE,
     'user/*/edit' => TRUE,
     'user/*/edit/*' => TRUE,
+    'user/*/translations' => TRUE,
+    'user/*/translations/*' => TRUE,
   );
   return $paths;
 }
