Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.119
diff -u -r1.119 system.install
--- modules/system/system.install	30 May 2007 08:08:58 -0000	1.119
+++ modules/system/system.install	31 May 2007 21:43:18 -0000
@@ -3339,6 +3339,15 @@
 
   return $ret;
 }
+
+function system_update_6023() {
+  $ret = array();
+
+  db_add_field($ret, 'node', 'snid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0));
+  db_add_field($ret, 'node', 'tr_status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
+  return $ret;
+}
+
 /**
  * @} End of "defgroup updates-5.x-to-6.x"
  * The next series of updates should start at 7000.
Index: modules/node/node.schema
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.schema,v
retrieving revision 1.1
diff -u -r1.1 node.schema
--- modules/node/node.schema	25 May 2007 12:46:45 -0000	1.1
+++ modules/node/node.schema	31 May 2007 21:43:16 -0000
@@ -16,7 +16,9 @@
       'comment'  => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
       'promote'  => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
       'moderate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
-      'sticky'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0)
+      'sticky'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+      'snid'     => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
+      'tr_status'=> array('type' => 'int', 'not null' => TRUE, 'default' => 0),
     ),
     'indexes' => array(
       'nid'                 => array('nid'),
@@ -28,7 +30,8 @@
       'node_title_type'     => array('title', array('type', 4)),
       'node_type'           => array(array('type', 4)),
       'status'              => array('status'),
-      'uid'                 => array('uid')
+      'uid'                 => array('uid'),
+      'snid'                => array('snid'),
     ),
     'unique keys' => array(
       'nid_vid' => array('nid', 'vid'),
Index: modules/translation/translation.module
===================================================================
RCS file: modules/translation/translation.module
diff -N modules/translation/translation.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/translation/translation.module	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,321 @@
+<?php
+// $Id: translation.module,v 1.0 Exp $
+
+/**
+ * @file
+ *   Manages content translations.
+ *
+ * @todo
+ *   - An option to toggle translation links for content types?
+ *   - Help text and documentation!
+ *   - New content type setting, to enable translation per node type?
+ *   - What if we want to add an existing node to a translation set?
+ *     hook_nodeapi() update and delete are ready for this, but no UI yet.
+ *   - Admin overview interface for translations not included,
+ *     because unfortunately it is mostly copy-paste of the node admin
+ *     overview (which is not modular enough yet). Translations work,
+ *     nonetheless.
+ */
+
+define ('TRANSLATION_ENABLED', 2);
+
+/**
+ * Implementation of hook_help().
+ */
+function translation_help($section) {
+  switch ($section) {
+    case 'admin/help#translation':
+      $output = '<p>'. t('The <em>translation</em> module allows content translation for multilingual sites.') .'</p>';
+      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@translation">Translation page</a>.', array('@translation' => 'http://drupal.org/handbook/modules/translation/')) .'</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function translation_menu() {
+  $items = array();
+  $items['node/%node/translate'] = array(
+    'title' => 'Translate',
+    'page callback' => 'translation_node_overview',
+    'page arguments' => array(1),
+    'access callback' => '_translation_tab_access',
+    'access arguments' => array(1),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 2,
+  );
+  return $items;
+}
+
+/**
+ * Menu access callback.
+ *
+ * Only display translation tab for node types, where language is enabled
+ * and where the current node is not language neutral (which should span
+ * all languages).
+ */
+function _translation_tab_access($node) {
+  if (!empty($node->language) && variable_get('language_' . $node->type, 0) == TRANSLATION_ENABLED) {
+    return user_access('translate content');
+  }
+  return FALSE;
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function translation_perm() {
+  return array('translate content');
+}
+
+/**
+ * Implementation of hook_form_alter().
+ *
+ * Alters language fields on node forms when a translation is about to be created.
+ */
+function translation_form_alter(&$form, $form_state, $form_id) {
+  if ($form_id == 'node_type_form') {
+    $form['workflow']['language']['#options'][TRANSLATION_ENABLED] = t('Enabled, with translation');
+// TODO : update the description
+//    $form['workflow']['language']['#description'] = '';
+  }
+  elseif ($form['#id'] == 'node-form' && variable_get('language_' . $form['#node']->type, 0) == TRANSLATION_ENABLED) {
+    $node = $form['#node'];
+    if (!empty($node->translation_source)) {
+      // We are creating a translation. Add values and lock language field.
+      $form['translation_source'] = array('#type' => 'value', '#value' => $node->translation_source);
+      $form['language']['#disabled'] = TRUE;
+    }
+    elseif (!empty($node->nid) && !empty($node->snid)) {
+      // 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 (translation_node_get_translations($node) as $translation) {
+        if ($translation->nid != $node->nid) {
+          unset($form['language']['#options'][$translation->language]);
+        }
+      }
+      // Add translation values and workflow options
+      $form['translation'] = array('#type' => 'fieldset',
+        '#title' => t('Translation settings'),
+        '#access' => user_access('translate content'),
+        '#collapsible' => TRUE,
+        '#collapsed' => !$node->tr_status,
+        '#tree' => TRUE,
+        '#weight' => 30,
+      );
+      if ($node->snid == $node->nid) {
+        // This is the source node of the translation
+        $form['translation']['retranslate'] = array('#type' => 'checkbox',
+          '#title' => t('Update translations'),
+          '#default_value' => 0,
+          '#description' => t('Mark translations of this content as outdated.'),
+        );
+        $form['translation']['status'] = array('#type' => 'value', '#value' => 0);
+      }
+      else {
+        $form['translation']['status'] = array('#type' => 'checkbox',
+          '#title' => t('This translation needs to be updated'),
+          '#default_value' => $node->tr_status,
+          '#description' => t('This option checked means that this translation needs to be updated because the source content has been updated. Uncheck when the translation is up to date.'),
+        );
+      }
+      $form['translation']['snid'] = array('#type' => 'value', '#value' => $node->snid);
+    }
+  }
+}
+
+/**
+ * Implementation of hook_link().
+ *
+ * Display translation links if this node is part of a translation set.
+ */
+function translation_link($type, $node = NULL, $teaser = FALSE) {
+  $links = array();
+  if ($type == 'node' && ($node->snid) && $translations = translation_node_get_translations($node)) {
+    // Do not show link to the same node.
+    unset($translations[$node->language]);
+    $languages = locale_language_list('native');
+    foreach ($translations as $language => $translation) {
+      $links["node_translation_$language"] = array(
+        'title' => $languages[$language],
+        'href' => "node/$translation->nid",
+        'attributes' => array('title' => $translation->title)
+      );
+    }
+  }
+  return $links;
+}
+
+/**
+ * Overview page for a node's translations.
+ *
+ * @param $node
+ *   Node object.
+ */
+function translation_node_overview($node) {
+  $translations = translation_node_get_translations($node);
+
+  $output = '<p>' . t('%title is in @language, so it is possible to translate it to the following languages enabled on your site:', array('%title' => $node->title, '@language' => locale_language_name($node->language))) . '</p>';
+
+  $header = array(t('Language'), t('Title'), t('Status'), t('Operations'));
+
+  foreach (language_list() as $language) {
+    $options = array();
+    $language_name = $language->name;
+    if (isset($translations[$language->language])) {
+      // We load the full node to check whether the user can edit it.
+      $trnode = node_load($translations[$language->language]->nid);
+      $title = l($trnode->title, 'node/'. $trnode->nid);
+      if (node_access('update', $trnode)) {
+        $options[] = l(t('edit'), "node/$trnode->nid/edit");
+      }
+      $status = $trnode->status ? t('Published') : t('Not published');
+      $status .= $trnode->tr_status ? ' - <span class="marker">'. t('outdated') .'</span>' : '';
+      if ($trnode->nid == $trnode->snid) {
+        $language_name = '<strong>'. $language_name . '</strong> (source)';
+      }
+    }
+    else {
+      $title = t('n/a');
+      if (node_access('create', $node)) {
+        $options[] = l(t('add translation'), 'node/add/'. $node->type, array('query' => "translation=$node->nid&language=$language->language"));
+      }
+      $status = t('Not translated');
+    }
+    $rows[] = array($language_name, $title, $status, implode(" | ", $options));
+  }
+  $output .= theme('table', $header, $rows);
+
+  drupal_set_title(t('Translations of %title', array('%title' => $node->title)));
+  return $output;
+}
+
+/**
+ * Get translations for a specific node id.
+ *
+ * @param $param
+ *   Either a node object or the 'snid' of the translation set.
+ * @return
+ *   Array of node translations indexed by language.
+ */
+function translation_node_get_translations($param) {
+  static $translations = array();
+  $snid = (is_object($param) && $param->snid) ? $param->snid : $param;
+  if (is_numeric($snid) && $snid) {
+    if (!isset($translations[$snid])) {
+      $translations[$snid] = array();
+      $result = db_query(db_rewrite_sql('SELECT * FROM {node} WHERE snid = %d'), $snid);
+      while ($node = db_fetch_object($result)) {
+        $translations[$snid][$node->language] = $node;
+      }
+    }
+    return $translations[$snid];
+  }
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ *
+ * Manages translation information for nodes.
+ */
+function translation_nodeapi(&$node, $op, $teaser, $page) {
+  // Only if languages are enabled for this node type.
+  if (variable_get('language_' . $node->type, 0)) {
+    switch ($op) {
+
+      case 'prepare':
+        if (empty($node->nid) && isset($_GET['translation']) && isset($_GET['language']) && ($source_nid = $_GET['translation']) && ($language = $_GET['language']) && (user_access('translate content'))) {
+          // We are translating a node from a source node, so
+          // load the node to be translated and populate fields.
+          $node->language = $language;
+          $node->translation_source = node_load($source_nid);
+          $node->title = $node->translation_source->title;
+          $node->body = $node->translation_source->body;
+          // Let every module add translated fields.
+          node_invoke_nodeapi($node, 'prepare translation');
+        }
+        break;
+
+      case 'insert':
+        if (!empty($node->translation_source)) {
+          if ($node->translation_source->snid) {
+            // Add node to existing translation set.
+            $snid = $node->translation_source->snid;
+          }
+          else {
+            // Create new translation set, using nid from the source node.
+            $snid = $node->translation_source->nid;
+            db_query("UPDATE {node} SET snid = %d, tr_status = %d WHERE nid = %d", $snid, 0, $node->translation_source->nid);
+          }
+          db_query("UPDATE {node} SET snid = %d, tr_status = %d WHERE nid = %d", $snid, 0, $node->nid);
+        }
+        break;
+
+      case 'update':
+        if (isset($node->translation) && $node->translation && !empty($node->language)) {
+          if ($node->snid) {
+            // Update translation information.
+            db_query("UPDATE {node} SET snid = %d, tr_status = %d WHERE nid = %d", $node->snid, $node->translation['status'], $node->nid);
+            if (!empty($node->translation['retranslate'])) {
+              // This is source content, requiring all translations to be marked outdated
+              db_query("UPDATE {node} SET tr_status = 1 WHERE snid = %d AND nid != %d", $node->snid, $node->nid);
+            }
+          }
+          else {
+            // The node may have been added to some existing translation set.
+            db_query("UPDATE {node} SET snid = %d, tr_status = %d WHERE nid = %d", $node->snid, $node->tr_status, $node->nid);
+          }
+        }
+        else {
+          // Remove node from translation set if existed.
+          translation_remove_from_set($node);
+        }
+        break;
+
+      case 'delete':
+        translation_remove_from_set($node);
+        break;
+    }
+  }
+}
+
+/**
+ * Remove a node from its translation set (if any)
+ * and update the set accordingly.
+ */
+function translation_remove_from_set($node) {
+  if ($node->snid) {
+    if (db_num_rows(db_query('SELECT * FROM {node} WHERE snid = %d', $node->snid)) <= 2) {
+      // There will be only one node left in the set : we remove all
+      db_query('UPDATE {node} SET snid = 0, tr_status = 0 WHERE snid = %d', $node->snid);
+    }
+    else {
+      db_query('UPDATE {node} SET snid = 0, tr_status = 0 WHERE nid = %d', $node->nid);
+
+      // If the node being removed was the source of the translation set,
+      // we pick a new source - preferably one that is up to date.
+      if ($node->snid == $node->nid) {
+        $new_snid = db_result(db_query('SELECT nid FROM {node} WHERE snid = %d ORDER BY tr_status ASC, nid ASC', $node->snid, $node->snid));
+        db_query('UPDATE {node} SET snid = %d WHERE snid = %d', $new_snid, $node->snid);
+      }
+    }
+  }
+}
+
+/**
+ * Return path of translations for a node, based on its path.
+ *
+ * @param $path
+ *   A Drupal path
+ */
+function translation_path_get_translations($path) {
+  $output = array();
+  // Check for a node related path, and for its translations.
+  if ((preg_match("/^(node\/)([0-9]+)(.*)$/", $path, $matches)) && ($node = node_load($matches[2])) && !empty($node->translation)) {
+    foreach (translation_node_get_translations($node) as $language => $trnode) {
+      $output[$language] = 'node/'.$trnode->nid.$matches[3];
+    }
+  }
+  return $output;
+}
Index: modules/translation/translation.info
===================================================================
RCS file: modules/translation/translation.info
diff -N modules/translation/translation.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/translation/translation.info	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,6 @@
+; $Id: translation.info Exp $
+name = Content translation
+description = Allows content to be translated into different languages.
+dependencies[] = locale
+package = Core - optional
+version = VERSION
