Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.783
diff -u -r1.783 comment.module
--- modules/comment/comment.module	10 Oct 2009 21:39:02 -0000	1.783
+++ modules/comment/comment.module	12 Oct 2009 17:49:50 -0000
@@ -1641,6 +1641,7 @@
 
   $op = isset($_POST['op']) ? $_POST['op'] : '';
   $node = node_load($comment->nid);
+  $form['#comment'] = $comment;
 
   if (!$user->uid && variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT) != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
     $form['#attached']['library'][] = array('system', 'cookie');
Index: modules/entity_translation/entity_translation.info
===================================================================
RCS file: modules/entity_translation/entity_translation.info
diff -N modules/entity_translation/entity_translation.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.info	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,15 @@
+; $Id$
+name = Entity translation
+description = Allow fieldable entities to be translated into different languages.
+dependencies[] = locale
+package = Core
+version = VERSION
+core = 7.x
+files[] = entity_translation.install
+files[] = entity_translation.module
+files[] = entity_translation.admin.inc
+files[] = entity_translation.handler.inc
+files[] = entity_translation.comment.inc
+files[] = entity_translation.node.inc
+files[] = entity_translation.taxonomy.inc
+files[] = entity_translation.user.inc
Index: modules/entity_translation/entity_translation.handler.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.handler.inc
diff -N modules/entity_translation/entity_translation.handler.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.handler.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,359 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * TODO
+ */
+
+/**
+ * TODO
+ */
+interface EntityTranslationHandler {
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function load();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function save();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function isTranslating();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function setTranslating($translating);
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function getTranslations();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function setTranslation($translation, $values = NULL);
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function removeTranslation($langcode = NULL);
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function initTranslations();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function setEntity($entity);
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function getLanguage();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function getOriginalLanguage();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function setOriginalLanguage($langcode);
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function setOutdated($outdated);
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function getBasePath();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function getEditPath();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function getHumanReadableId();
+
+  /**
+   * TODO
+   *
+   * @return unknown_type
+   */
+  public function getAccess($op);
+}
+
+/**
+ * TODO
+ */
+abstract class EntityTranslationDefaultHandler implements EntityTranslationHandler {
+
+  protected $entityType;
+  protected $entity;
+  protected $entityInfo;
+  protected $entityId;
+
+  private $translating;
+  private $outdated;
+
+  public function __construct($entity_type, $entity_info, $entity, $entity_id) {
+    $this->entityType = $entity_type;
+    $this->entityInfo = $entity_info;
+    $this->entity = $entity;
+    $this->entityId = $entity_id;
+    
+    $this->translating = FALSE;
+    $this->outdated = FALSE;
+  }
+
+  public function load() {
+    $translations_key = $this->getTranslationsKey();
+    $this->entity->{$translations_key} = $this->readTranslations();
+  }
+
+  public function save() {
+    $this->writeTranslations();
+  }
+
+  public function initTranslations() {
+    $langcode = $this->getLanguage();
+
+    if (!empty($langcode)) {
+      $translation = array('language' => $langcode, 'status' => $this->getStatus());
+      $this->setTranslation($translation);
+      $this->setOriginalLanguage($langcode);
+    }
+  }
+
+  public function setEntity($entity) {
+    $this->entity = $entity;
+  }
+
+  public function getTranslations() {
+    $translations_key = $this->getTranslationsKey();
+
+    if (!isset($this->entity->{$translations_key})) {
+      $this->load();
+    }
+
+    return $this->entity->{$translations_key};
+  }
+
+  public function setTranslation($translation, $values = NULL) {
+    if (isset($translation['source']) && $translation['language'] == $translation['source']) {
+      throw new Exception('Invalid translation language');
+    }
+
+    $translations = $this->getTranslations();
+    $langcode = $translation['language'];
+
+    $this->setTranslating(TRUE);
+
+    if (isset($translations->data[$langcode])) {
+      // FIXME: unset($translation['source']);
+      $translation = array_merge($translations->data[$langcode], $translation);
+      $translation['changed'] = REQUEST_TIME;
+    }
+
+    $translations->data[$langcode] = $translation;
+
+    if (is_array($values)) {
+      // Update field translations.
+      list(, , $bundle) = field_extract_ids($this->entityType, $this->entity);
+      foreach (field_info_instances($bundle) as $instance) {
+        $field_name = $instance['field_name'];
+        $field = field_info_field($field_name);
+        if ($field['translatable'] && isset($values[$field_name])) {
+          $this->entity->{$field_name}[$langcode] = $values[$field_name][$langcode];
+        }
+      }
+    }
+  }
+
+  public function removeTranslation($langcode = NULL) {
+    $translations_key = $this->getTranslationsKey();
+
+    if (!empty($langcode)) {
+      unset($this->entity->{$translations_key}->data[$langcode]);
+    }
+    else {
+      $this->entity->{$translations_key}->data = array();
+    }
+
+    // Remove field translations.
+    list(, , $bundle) = field_extract_ids($this->entityType, $this->entity);
+    foreach (field_info_instances($bundle) as $instance) {
+      $field_name = $instance['field_name'];
+      $field = field_info_field($field_name);
+      if ($field['translatable']) {
+        if (!empty($langcode)) {
+          $this->entity->{$field_name}[$langcode] = array();
+        }
+        else {
+          $this->entity->{$field_name} = array();
+        }
+      }
+    }
+  }
+
+  public function getLanguage() {
+    return language_default()->language;
+  }
+
+  public function setLanguage($langcode) {
+    $this->language = $langcode;
+  }
+
+  public function isTranslating() {
+    return $this->translating;
+  }
+
+  public function setTranslating($translating) {
+    $this->translating = $translating;
+  }
+
+  public function getOriginalLanguage() {
+    return $this->getTranslations()->original;
+  }
+
+  public function setOriginalLanguage($langcode) {
+    $translations = $this->getTranslations();
+
+    if (isset($translations->original) && $translations->original != $langcode) {
+      $translations->data[$langcode] = $translations->data[$translations->original];
+      $translations->data[$langcode]['language'] = $langcode;
+      unset($translations->data[$translations->original]);
+    }
+
+    $translations->original = $langcode;
+  }
+
+  public function setOutdated($outdated) {
+    $this->outdated = $outdated;
+  }
+
+  public function getHumanReadableId() {
+    return "{$this->entityType}:{$this->getId()}";
+  }
+
+  public function getAccess($op) {
+    return TRUE;
+  }
+
+  protected function getTranslationsKey() {
+    return $this->entityInfo['object keys']['translations'];
+  }
+
+  protected function getStatus() {
+    return TRUE;
+  }
+
+  protected function getId() {
+    return $this->entityId;
+  }
+
+  protected function readTranslations() {
+    // FIXME!
+    $etid = _field_sql_storage_etid($this->entityType);
+
+    $results = db_select('entity_translation', 'et')
+      ->fields('et')
+      ->condition('etid', $etid)
+      ->condition('entity_id', $this->entityId)
+      ->orderBy('created')
+      ->execute();
+
+    $translations = array();
+    foreach ($results as $row) {
+      $translations[$row->language] = (array) $row;
+      // Only the original translation has an empty source.
+      if (empty($row->source)) {
+        $original = $row->language;
+      }
+    }
+
+    return (object) array('original' => isset($original) ? $original : NULL, 'data' => $translations);
+  }
+
+  protected function writeTranslations() {
+    // FIXME!
+    $etid = _field_sql_storage_etid($this->entityType);
+
+    // Delete and insert, rather than update, in case a value was added.
+    db_delete('entity_translation')
+      ->condition('etid', $etid)
+      ->condition('entity_id', $this->entityId)
+      ->execute();
+
+    // TODO: Multi-insert.
+    global $user;
+    $translations = $this->getTranslations();
+
+    foreach ($translations->data as $langcode => $translation) {
+      $translation += array(
+        'etid' => $etid,
+        'entity_id' => $this->entityId,
+        'source' => '',
+        'uid' => $user->uid,
+        'translate' => FALSE,
+        'status' => FALSE,
+        'created' => REQUEST_TIME,
+        'changed' => REQUEST_TIME,
+      );
+
+      if ($this->outdated && $langcode != $translations->original) {
+        $translation['translate'] = TRUE;
+      }
+
+      drupal_write_record('entity_translation', $translation);
+    }
+  }
+}
Index: modules/entity_translation/entity-translation-node-form.js
===================================================================
RCS file: modules/entity_translation/entity-translation-node-form.js
diff -N modules/entity_translation/entity-translation-node-form.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity-translation-node-form.js	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,15 @@
+// $Id$
+
+(function ($) {
+
+Drupal.behaviors.translationNodeFieldsetSummaries = {
+  attach: function (context) {
+    $('fieldset#edit-entity-translation-node', context).setSummary(function (context) {
+      return $('#edit-entity-translation-node-retranslate', context).is(':checked') ?
+        Drupal.t('Flag translations as outdated') :
+        Drupal.t('Don\'t flag translations as outdated');
+    });
+  }
+};
+
+})(jQuery);
Index: modules/entity_translation/entity_translation.comment.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.comment.inc
diff -N modules/entity_translation/entity_translation.comment.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.comment.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,29 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * TODO
+ */
+
+/**
+ * TODO
+ */
+class EntityTranslationCommentHandler extends EntityTranslationDefaultHandler {
+
+  public function __construct($entity_type, $entity_info, $entity, $entity_id) {
+    parent::__construct('comment', $entity_info, $entity, $entity_id);
+  }
+  
+  public function getBasePath() {
+    return "comment/{$this->getId()}";
+  }
+
+  public function getEditPath() {
+    return "comment/edit/{$this->getId()}";
+  }
+
+  public function getHumanReadableId() {
+    return $this->entity->subject;
+  }
+}
Index: modules/entity_translation/entity_translation.module
===================================================================
RCS file: modules/entity_translation/entity_translation.module
diff -N modules/entity_translation/entity_translation.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.module	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,312 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * TODO
+ */
+
+require_once DRUPAL_ROOT . '/modules/entity_translation/entity_translation.handler.inc';
+require_once DRUPAL_ROOT . '/modules/entity_translation/entity_translation.comment.inc';
+require_once DRUPAL_ROOT . '/modules/entity_translation/entity_translation.node.inc';
+require_once DRUPAL_ROOT . '/modules/entity_translation/entity_translation.taxonomy.inc';
+require_once DRUPAL_ROOT . '/modules/entity_translation/entity_translation.user.inc';
+
+
+/* Hook implementations */
+
+/**
+ * Implement hook_translation_info()
+ */
+function entity_translation_translation_info($types) {
+  $info = array(
+    'comment' => array(
+      'translation' => array(
+        'entity_translation' => array(
+          'class' => 'EntityTranslationCommentHandler',
+          'path' => 'comment/edit/%comment',
+          'edit form' => array(
+            'form id' =>  'comment-form',
+            'entity key' => '#comment',
+          ),
+        ),
+      ),
+    ),
+    'node' => array(
+      'translation' => array(
+        'entity_translation' => array(
+          'class' => 'EntityTranslationNodeHandler',
+          'path' => 'node/%node',
+          'access callback' => 'entity_translation_node_tab_access',
+          'access arguments' => array(1),
+          'edit form' => array(
+            'form id' =>  'node-form',
+            'entity key' => '#node',
+          ),
+        ),
+      ),
+    ),
+    'taxonomy_term' => array(
+      'translation' => array(
+        'entity_translation' => array(
+          'class' => 'EntityTranslationTaxonomyTermHandler',
+          'path' => 'taxonomy/term/%taxonomy_term',
+          'edit form' => array(
+            'form id' =>  'taxonomy-form-term',
+            'entity key' => '#term',
+          ),
+        ),
+      ),
+    ),
+    'user' => array(
+      'translation' => array(
+        'entity_translation' => array(
+          'class' => 'EntityTranslationUserHandler',
+          'path' => 'user/%user',
+          'edit form' => array(
+            'form id' =>  'user-profile-form',
+            'entity key' => '#user',
+          ),
+        ),
+      ),
+    ),
+  );
+  
+  return array_intersect_key($info, $types);
+}
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function entity_translation_entity_info_alter(&$entity_info) {
+  // Collect entity-specific translation information.
+  $types = array_flip(array_keys($entity_info));
+  $translation_info = module_invoke_all('translation_info', $types);
+  $entity_info = array_merge_recursive($entity_info, $translation_info);
+  $edit_form_info = array();
+
+  foreach (field_info_fieldable_types() as $entity_type => $info) {
+    // Provide defaults for translation info.
+    if (!isset($entity_info[$entity_type]['translation']['entity_translation'])) {
+      $entity_info[$entity_type]['translation']['entity_translation'] = array();
+    }
+    $entity_info[$entity_type]['translation']['entity_translation'] += array(
+      'access callback' => 'entity_translation_tab_access',
+      'theme callback' => 'variable_get',
+      'theme arguments' => array('admin_theme'),
+    );
+    $entity_info[$entity_type]['object keys'] += array(
+      'translations' => 'translations',
+    );
+
+    // Prepare edit form info.
+    if (isset($entity_info[$entity_type]['translation']['entity_translation']['edit form'])) {
+      $info = $entity_info[$entity_type]['translation']['entity_translation']['edit form'];
+      $form_id = $info['form id'];
+      $edit_form_info[$form_id] = $info;
+      $edit_form_info[$form_id]['entity type'] = $entity_type;
+    }
+  }
+
+  variable_set('entity_translation_edit_form_info', $edit_form_info);
+}
+
+/**
+ * Implement hook_menu().
+ */
+function entity_translation_menu() {
+  $items = array();
+
+  // Create tabs for all possible bundles.
+  foreach (field_info_fieldable_types() as $entity_type => $info) {
+    if (isset($info['translation']['entity_translation']['path'])) {
+      // Extract informations from the bundle description.
+      $path = $info['translation']['entity_translation']['path'];
+      $keys = array('theme callback', 'theme arguments', 'access callback', 'access arguments');
+      $item = array_intersect_key($info['translation']['entity_translation'], drupal_map_assoc($keys));
+      $entity_position = count(explode('/', $path)) - 1;
+
+      $items["$path/entity-translation"] = array(
+        'title' => 'Translate',
+        'page callback' => 'entity_translation_overview',
+        'page arguments' => array($entity_type, $entity_position),
+        'type' => MENU_LOCAL_TASK,
+        'weight' => 2,
+        'file' => 'entity_translation.admin.inc',
+      ) + $item;
+
+      $items["$path/entity-translation/overview"] = array(
+        'title' => 'Overview',
+        'type' => MENU_DEFAULT_LOCAL_TASK,
+        'weight' => 0,
+      );
+
+      $items["$path/entity-translation/translate/%entity_translation_language/%entity_translation_language"] = array(
+        'title' => 'Translate',
+        'page callback' => 'drupal_get_form',
+        'page arguments' => array('entity_translation_translation_form', $entity_type, $entity_position, $entity_position + 3, $entity_position + 4),
+        'type' => MENU_LOCAL_TASK,
+        'weight' => 1,
+        'file' => 'entity_translation.admin.inc',
+      ) + $item;
+
+      $items["$path/entity-translation/delete/%entity_translation_language/%entity_translation_language"] = array(
+        'title' => 'Delete',
+        'page callback' => 'drupal_get_form',
+        'page arguments' => array('entity_translation_translation_delete_confirm', $entity_type, $entity_position, $entity_position + 3, $entity_position + 4),
+        'file' => 'entity_translation.admin.inc',
+      ) + $item;
+    }
+  }
+
+  return $items;
+}
+
+/**
+ * Implement hook_permission().
+ */
+function entity_translation_permission() {
+  return array(
+    'entity translation' => array(
+      'title' => t('Translate entities'),
+      'description' => t('Translate field content for any fieldable entity.'),
+    ),
+  );
+}
+
+/**
+ * Implement hook_field_attach_pre_insert().
+ */
+function entity_translation_field_attach_pre_insert($obj_type, $object) {
+  $handler = entity_translation_get_handler($obj_type, $object);
+  $handler->initTranslations();
+  $handler->save();
+}
+
+/**
+ * Implement hook_field_attach_pre_update().
+ */
+function entity_translation_field_attach_pre_update($obj_type, $object) {
+  $handler = entity_translation_get_handler($obj_type, $object, TRUE);
+  $langcode = $handler->getLanguage();
+
+  // Only create a translation on edit if the translation set is empty:
+  // the node might have been created with language set to "language neutral".
+  if (empty($handler->getTranslations()->data)) {
+    $handler->initTranslations();
+  }
+  elseif (!empty($langcode) && !$handler->isTranslating()) {
+    $handler->setOriginalLanguage($langcode);
+  }
+
+  $handler->save();
+}
+
+/**
+ * Implement hook_field_attach_delete().
+ */
+function entity_translation_field_attach_delete($obj_type, $object) {
+  $handler = entity_translation_get_handler($obj_type, $object);
+  $handler->removeTranslation();
+  $handler->save();
+}
+
+
+/* Forms */
+
+/**
+ * Implement hook_form_alter().
+ */
+function entity_translation_form_alter(&$form, $form_state) {
+  $info = entity_translation_edit_form_info($form);
+
+  if ($info) {
+    $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
+    $translations = $handler->getTranslations();
+
+    if (!empty($translations->data)) {
+      $form['entity_translation_node'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Translation'),
+        '#collapsible' => TRUE,
+        '#tree' => TRUE,
+      );
+
+      $form['entity_translation_node']['retranslate'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Flag translations as outdated'),
+        '#default_value' => 0,
+        '#description' => t('If you made a significant change, which means translations should be updated, you can flag all translations of this post as outdated. This will not change any other property of those posts, like whether they are published or not.'),
+      );
+
+      array_unshift($form['#submit'], 'entity_translation_edit_form_submit');
+    }
+
+    // FIXME!
+    $function = "entity_translation_{$info['entity type']}_form_alter";
+    if (function_exists($function)) {
+      $function($form, $form_state, $handler);
+    }
+  }
+}
+
+/**
+ * Edit form submit handler.
+ */
+function entity_translation_edit_form_submit($form, &$form_state) {
+  $info = entity_translation_edit_form_info($form);
+  $handler = entity_translation_get_handler($info['entity type'], $info['entity']);
+  $outdated = (bool) $form_state['values']['entity_translation_node']['retranslate'];
+  $handler->setOutdated($outdated);
+}
+
+
+/* Helper functions */
+
+/**
+ * Entity translation handler factory.
+ */
+function entity_translation_get_handler($entity_type, $entity, $update = FALSE) {
+  $handlers =& drupal_static(__FUNCTION__);
+  list($entity_id) = field_extract_ids($entity_type, $entity);
+
+  if (!isset($handlers[$entity_type][$entity_id])) {
+    $entity_info = entity_get_info($entity_type);
+    $class = $entity_info['translation']['entity_translation']['class'];
+    $handlers[$entity_type][$entity_id] = new $class($entity_type, $entity_info, $entity, $entity_id);
+  }
+  else if ($update) {
+    $handlers[$entity_type][$entity_id]->setEntity($entity);
+  }
+
+  return $handlers[$entity_type][$entity_id];
+}
+
+function entity_translation_edit_form_info($form) {
+  $edit_form_info = variable_get('entity_translation_edit_form_info', array());
+
+  if (isset($edit_form_info[$form['#id']])) {
+    $info = $edit_form_info[$form['#id']];
+    if (isset($form[$info['entity key']])) {
+      $info['entity'] = (object) $form[$info['entity key']];
+      return $info;
+    }
+  }
+
+  return FALSE;
+}
+
+/**
+ * Access callback.
+ */
+function entity_translation_tab_access() {
+  return variable_get('language_count', 1) > 1 && user_access('entity translation');
+}
+
+/**
+ * Menu loader callback.
+ */
+function entity_translation_language_load($langcode) {
+  $enabled_languages = field_multilingual_content_languages();
+  return in_array($langcode, $enabled_languages) ? $langcode : FALSE;
+}
Index: modules/entity_translation/entity_translation.user.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.user.inc
diff -N modules/entity_translation/entity_translation.user.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.user.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,29 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * TODO
+ */
+
+/**
+ * TODO
+ */
+class EntityTranslationUserHandler extends EntityTranslationDefaultHandler {
+
+  public function __construct($entity_type, $entity_info, $entity, $entity_id) {
+    parent::__construct('user', $entity_info, $entity, $entity_id);
+  }
+
+  public function getBasePath() {
+    return "user/{$this->getId()}";
+  }
+
+  public function getEditPath() {
+    return "user/{$this->getId()}/edit";
+  }
+
+  public function getHumanReadableId() {
+    return $this->entity->name;
+  }
+}
Index: modules/entity_translation/entity_translation.admin.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.admin.inc
diff -N modules/entity_translation/entity_translation.admin.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.admin.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,295 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * TODO
+ */
+
+/**
+ * Translations overview menu callback.
+ */
+function entity_translation_overview($entity_type, $entity) {
+  $handler = entity_translation_get_handler($entity_type, $entity);
+
+  // Initialize translations if they are empty.
+  $translations = $handler->getTranslations();
+  if (empty($translations->original)) {
+    $handler->initTranslations();
+    $handler->save();
+  }
+
+  $header = array(t('Language'), t('Source language'), t('Title'), t('Status'), t('Operations'));
+  $languages = language_list();
+  $source = isset($_SESSION['entity_translation_source_language']) ? $_SESSION['entity_translation_source_language'] :  $translations->original;
+  $basePath = $handler->getBasePath();
+  $title = $handler->getHumanReadableId();
+
+  foreach ($languages as $language) {
+    $options = array();
+    $language_name = $language->name;
+    $langcode = $language->language;
+
+    if (isset($translations->data[$langcode])) {
+      list($id, $vid, $bundle) = field_extract_ids($entity_type, $entity);
+
+      // Existing translation in the translation set: display status.
+      $is_original = $langcode ==  $translations->original;
+      $translation = $translations->data[$langcode];
+      $path = "$basePath/entity-translation/translate/{$translations->original}/$langcode";
+      // TODO: title as translatable field and content language negotiation
+      $rowTitle = l($is_original ? $title : t('view'), $basePath, array('language' => $language));
+
+      if ($handler->getAccess('update')) {
+        $options[] = l(t('edit'), $is_original ? $handler->getEditPath() : $path);
+      }
+
+      $status = $translation['status'] ? t('Published') : t('Not published');
+      $status .= isset($translation['translate']) && $translation['translate'] ? ' - <span class="marker">' . t('outdated') . '</span>' : '';
+
+      if ($is_original) {
+        $language_name = t('<strong>@language_name</strong> ', array('@language_name' => $language_name));
+        $source_name = t('(original content)');
+      }
+      else {
+        $source_name = $languages[$translation['source']]->name;
+      }
+    }
+    else {
+      // No such translation in the set yet: help user to create it.
+      $rowTitle = $source_name = t('n/a');
+      $path = "$basePath/entity-translation/translate/$source/$langcode";
+
+      if ($source != $langcode && $handler->getAccess('update')) {
+        list(, , $bundle) = field_extract_ids($entity_type, $entity);
+        $translatable = FALSE;
+
+        foreach (field_info_instances($bundle) as $instance) {
+          $field_name = $instance['field_name'];
+          $field = field_info_field($field_name);
+          if ($field['translatable']) {
+            $translatable = TRUE;
+            break;
+          }
+        }
+
+        $options[] = $translatable ? l(t('add translation'), $path) : t('No translatable fields');
+      }
+      $status = t('Not translated');
+    }
+    $rows[] = array($language_name, $source_name, $rowTitle, $status, implode(" | ", $options));
+  }
+
+  drupal_set_title(t('Translations of %title', array('%title' => $title)), PASS_THROUGH);
+
+  $build['translation_node_overview'] = array(
+    '#theme' => 'table',
+    '#header' => $header,
+    '#rows' => $rows,
+  );
+
+  return $build;
+}
+
+/**
+ * Translation adding/editing form.
+ */
+function entity_translation_translation_form($form, $form_state, $entity_type, $entity, $source, $langcode) {
+  $handler = entity_translation_get_handler($entity_type, $entity);
+
+  $languages = language_list();
+  $args = array('@title' => $handler->getHumanReadableId(), '@language' => t($languages[$langcode]->name));
+  drupal_set_title(t('@title [@language translation]', $args));
+
+  $translations = $handler->getTranslations();
+  $new_translation = !isset($translations->data[$langcode]);
+
+  $form = array(
+    '#handler' => $handler,
+    '#entity_type' => $entity_type,
+    '#entity' => $entity,
+    '#source' => $source,
+    '#language' => $langcode,
+  );
+
+  // Display source language selector only if we are creating a new translation
+  // and there are at least two translations available.
+  if ($new_translation && count($translations->data) > 1) {
+    $form['source_language'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Source language'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#tree' => TRUE,
+      '#weight' => -4,
+      'language' => array(
+        '#type' => 'select',
+        '#default_value' => $source,
+        '#options' => array(),
+      ),
+      'submit' => array(
+        '#type' => 'submit',
+        '#value' => t('Change'),
+        '#submit' => array('entity_translation_translation_form_source_language_submit'),
+      ),
+    );
+    foreach (language_list() as $language) {
+      if (isset($translations->data[$language->language])) {
+        $form['source_language']['language']['#options'][$language->language] = t($language->name);
+      }
+    }
+  }
+
+  $translate = isset($translations->data[$langcode]) && $translations->data[$langcode]['translate'];
+
+  $form['translation'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Translation settings'),
+    '#collapsible' => TRUE,
+    '#collapsed' => !$translate,
+    '#tree' => TRUE,
+    '#weight' => -4,
+  );
+  $form['translation']['status'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('This translation is published'),
+    '#default_value' => isset($translations->data[$langcode]) && $translations->data[$langcode]['status'],
+    '#description' => t('When this option is unchecked, this translation will not be visible for non-administrators.'),
+  );
+  $form['translation']['translate'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('This translation needs to be updated'),
+    '#default_value' => $translate,
+    '#description' => t('When this option is checked, this translation needs to be updated because the source post has changed. Uncheck when the translation is up to date again.'),
+    '#disabled' => !$translate,
+  );
+
+  // If we are creating a new translation we need to retrieve form elements
+  // populated with the source language values.
+  $field_view = field_attach_view($entity_type, $entity, 'full', $langcode);
+  $source_form = array();
+  if ($new_translation) {
+    $source_form_state = $form_state;
+    field_attach_form($entity_type, $entity, $source_form, $source_form_state, $source);
+  }
+  field_attach_form($entity_type, $entity, $form, $form_state, $langcode);
+  list(, , $bundle) = field_extract_ids($entity_type, $entity);
+
+  foreach (field_info_instances($bundle) as $instance) {
+    $field_name = $instance['field_name'];
+    $field = field_info_field($field_name);
+    // If a field is not translatable it should not be editable from the
+    // translation form, yet it could be useful to display its value.
+    if (!$field['translatable']) {
+      $form[$field_name] = array(
+        '#markup' => drupal_render($field_view[$field_name]),
+        // Place the element where it would appear if displayed.
+        '#weight' => $instance['weight'],
+      );
+    }
+    // If we are creating a new translation we have to change the form item
+    // language information from source to target language, this way the
+    // user can find the form items already populated with the source values
+    // while the field form element holds the correct language information.
+    else if ($new_translation && !isset($entity->{$field_name}[$langcode]) && isset($source_form[$field_name][$source])) {
+      $form[$field_name][$langcode] = $source_form[$field_name][$source];
+    }
+  }
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save translation'),
+    '#submit' => array('entity_translation_translation_form_save_submit'),
+  );
+
+  if (!$new_translation) {
+    $form['delete'] = array(
+      '#type' => 'submit',
+      '#value' => t('Delete translation'),
+      '#submit' => array('entity_translation_translation_form_delete_submit'),
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Source language selector submit handler.
+ */
+function entity_translation_translation_form_source_language_submit($form, &$form_state) {
+  $handler = $form['#handler'];
+  $langcode = $form_state['values']['source_language']['language'];
+  $path = "{$handler->getBasePath()}/entity-translation/translate/$langcode/{$form['#language']}";
+  $form_state['redirect'] = array('path' => $path);
+  $languages = language_list();
+  drupal_set_message(t('Source translation set to: %language', array('%language' => t($languages[$langcode]->name))));
+}
+
+/**
+ * Translation saving submit handler.
+ */
+function entity_translation_translation_form_save_submit($form, &$form_state) {
+  $handler = $form['#handler'];
+
+  $translation = array(
+    'translate' => $form_state['values']['translation']['translate'],
+    'status' => $form_state['values']['translation']['status'],
+    'language' => $form['#language'],
+    'source' => $form['#source'],
+  );
+
+  $handler->setTranslation($translation, $form_state['values']);
+  $handler->save();
+
+  // Save field translations.
+  field_attach_update($form['#entity_type'], $form['#entity']);
+
+  $form_state['redirect'] = "{$handler->getBasePath()}/entity-translation";
+}
+
+/**
+ * Translation deletion submit handler.
+ */
+function entity_translation_translation_form_delete_submit($form, &$form_state) {
+  $form_state['redirect'] = "{$form['#handler']->getBasePath()}/entity-translation/delete/{$form['#source']}/{$form['#language']}";
+}
+
+/**
+ * Translation deletion confirmation form.
+ */
+function entity_translation_translation_delete_confirm($form, $form_state, $entity_type, $entity, $source, $langcode) {
+  $handler = entity_translation_get_handler($entity_type, $entity);
+  $languages = language_list();
+
+  $form = array(
+    '#handler' => $handler,
+    '#entity_type' => $entity_type,
+    '#entity' => $entity,
+    '#language' => $langcode,
+  );
+
+  return confirm_form($form,
+    t('Are you sure you want to delete the @language translation of %title?',
+      array('@language' => $languages[$langcode]->name, '%title' => $handler->getHumanReadableId())),
+    "{$handler->getBasePath()}/entity-translation/translate/$source/$langcode",
+    t('This action cannot be undone.'),
+    t('Delete'),
+    t('Cancel')
+  );
+}
+
+/**
+ * Translation deletion confirmation submit handler.
+ */
+function entity_translation_translation_delete_confirm_submit($form, &$form_state) {
+  $handler = $form['#handler'];
+
+  $handler->removeTranslation($form['#language']);
+  field_attach_update($form['#entity_type'], $form['#entity']);
+  
+  if (isset($_SESSION['entity_translation_source_language']) && $form['#language'] == $_SESSION['entity_translation_source_language']) {
+    unset($_SESSION['entity_translation_source_language']);
+  }
+
+  $form_state['redirect'] = "{$handler->getBasePath()}/entity-translation";
+}
Index: modules/entity_translation/entity_translation.taxonomy.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.taxonomy.inc
diff -N modules/entity_translation/entity_translation.taxonomy.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.taxonomy.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,29 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * TODO
+ */
+
+/**
+ * TODO
+ */
+class EntityTranslationTaxonomyTermHandler extends EntityTranslationDefaultHandler {
+
+  public function __construct($entity_type, $entity_info, $entity, $entity_id) {
+    parent::__construct('taxonomy_term', $entity_info, $entity, $entity_id);
+  }
+
+  public function getBasePath() {
+    return "taxonomy/term/{$this->getId()}";
+  }
+
+  public function getEditPath() {
+    return "taxonomy/term/{$this->getId()}/edit";
+  }
+
+  public function getHumanReadableId() {
+    return $this->entity->name;
+  }
+}
Index: modules/entity_translation/entity_translation.node.inc
===================================================================
RCS file: modules/entity_translation/entity_translation.node.inc
diff -N modules/entity_translation/entity_translation.node.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.node.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,113 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * TODO
+ */
+
+/**
+ * Implement hook_form_alter().
+ */
+function entity_translation_node_form_alter(&$form, $form_state, $handler) {
+  $translations = $handler->getTranslations(); 
+  
+  // Disable languages for existing translations, so it is not possible to switch this
+  // node to some language which is already in the translation set.
+  foreach ($translations->data as $langcode => $translation) {
+    if ($langcode != $translations->original) {
+      unset($form['language']['#options'][$langcode]);
+    }
+  }
+  if (count($translations->data) > 1) {
+    unset($form['language']['#options']['']);
+  }
+
+  if (isset($form['entity_translation_node'])) {
+    $form['entity_translation_node'] += array(
+      '#group' => 'additional_settings',
+      '#weight' => 100,
+      '#attached' => array(
+        'js' => array(drupal_get_path('module', 'entity_translation') . '/entity-translation-node-form.js'),
+      ),
+    );
+  }
+}
+
+/**
+ * Implement hook_node_load().
+ */
+function entity_translation_node_load($nodes) {
+  // TODO: multiple load.
+  foreach ($nodes as $node) {
+    entity_translation_get_handler('node', $node)->load();
+  }
+}
+
+/**
+ * Implement hook_node_view().
+ */
+function entity_translation_node_view($node, $build_mode) {
+  if (!empty($node->translations)) {
+    $translations = $node->translations->data;
+    $languages = language_list();
+    global $language;
+    foreach ($languages as $langcode => $target_language) {
+      if (isset($translations[$langcode]) && $langcode != $language->language) {
+        $links["entity_translation_node_$langcode"] = array(
+          'title' => $target_language->native,
+          'href' => 'node/' . $node->nid,
+          'language' => $target_language,
+          // TODO: 'title' => $translations[$langcode]->title
+          'attributes' => array('class' => array('translation-link')),
+        );
+        $node->content['links']['translation'] = array(
+          '#theme' => 'links',
+          '#links' => $links,
+          '#attributes' => array('class' => 'links inline'),
+        );
+      }
+    }
+  }
+}
+
+/**
+ * TODO
+ */
+function entity_translation_node_tab_access($node) {
+  return !empty($node->language) && entity_translation_tab_access();
+}
+
+/**
+ * TODO
+ */
+class EntityTranslationNodeHandler extends EntityTranslationDefaultHandler {
+
+  public function __construct($entity_type, $entity_info, $entity, $entity_id) {
+    parent::__construct('node', $entity_info, $entity, $entity_id);
+  }
+
+  public function getLanguage() {
+    return $this->entity->language;
+  }
+
+  public function getBasePath() {
+    return "node/{$this->getId()}";
+  }
+
+  public function getEditPath() {
+    return "node/{$this->getId()}/edit";
+  }
+
+  public function getHumanReadableId() {
+    return $this->entity->title[FIELD_LANGUAGE_NONE][0]['value'];
+  }
+
+  public function getAccess($op) {
+    return node_access($op, $this->entity);
+  }
+
+  protected function getStatus() {
+    return (boolean) $this->entity->status;
+  }
+}
Index: modules/entity_translation/entity_translation.install
===================================================================
RCS file: modules/entity_translation/entity_translation.install
diff -N modules/entity_translation/entity_translation.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/entity_translation/entity_translation.install	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,84 @@
+<?php
+// $Id$
+
+/**
+ * Implement hook_install().
+ */
+function entity_translation_install() {
+  // entity_translation_form_alter() needs to run after locale's implementation.
+  db_update('system')
+    ->fields(array('weight' => 1))
+    ->condition('name', 'entity_translation')
+    ->execute();
+}
+
+/**
+ * Implement hook_schema().
+ */
+function entity_translation_schema() {
+  $schema['entity_translation'] = array(
+    'description' => 'Table to track entity translations',
+    'fields' => array(
+      'etid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'The entity type id this translation relates to',
+      ),
+      'entity_id' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'The entity id this translation relates to',
+      ),
+      // TODO: Consider an integer field for 'language'.
+      'language' => 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.',
+      ),
+      'uid' => array(
+        'description' => 'The author of this translation.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'status' => array(
+        'description' => 'Boolean indicating whether the translation is published (visible to non-administrators).',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 1,
+      ),
+      'translate' => array(
+        'description' => 'A boolean indicating whether this translation needs to be updated.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'created' => array(
+        'description' => 'The Unix timestamp when the translation was created.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'changed' => array(
+        'description' => 'The Unix timestamp when the translation was most recently saved.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('etid', 'entity_id', 'language'),
+  );
+
+  return $schema;
+}
