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,344 @@
+<?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.
+ */
+
+/**
+ * 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)) {
+    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_id) {
+  if ($form['#id'] == 'node-form' && variable_get('language_' . $form['#node']->type, 0)) {
+    $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->translation)) {
+      // 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->translation['status'],
+        '#tree' => TRUE,
+        '#weight' => 30,
+      );
+      if ($node->translation['trid'] == $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->translation['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']['trid'] = array('#type' => 'value', '#value' => $node->translation['trid']);
+      $form['translation']['snid'] = array('#type' => 'value', '#value' => $node->translation['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' && !empty($node->translation) && $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'));
+
+  $languages = language_list();
+  unset($languages[$node->language]);
+  foreach ($languages as $language) {
+    $options = array();
+    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');
+      // If current node is source, add marker for older versions.
+      $status .= ($trnode->translation['status'] ? ' <span class="marker">'. t('outdated') .'</span>' : '');
+    }
+    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 'trid' of the translation set.
+ * @return
+ *   Array of node translations indexed by language.
+ */
+function translation_node_get_translations($param) {
+  static $translations = array();
+  $trid = (is_object($param) && !empty($param->translation['trid']) ? $param->translation['trid'] : $param);
+  if (is_numeric($trid) && $trid) {
+    if (!isset($translations[$trid])) {
+      $translations[$trid] = array();
+      $result = db_query(db_rewrite_sql('SELECT n.* FROM {node} n INNER JOIN {translation} t ON n.nid = t.nid WHERE t.trid = %d'), $trid);
+      while ($node = db_fetch_object($result)) {
+        $translations[$trid][$node->language] = $node;
+      }
+    }
+    return $translations[$trid];
+  }
+}
+
+/**
+ * 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 'load':
+        // Add translation information to the node.
+        return array('translation' => db_fetch_array(db_query('SELECT * FROM {translation} WHERE nid = %d', $node->nid)));
+        break;
+
+      case 'insert':
+        if (!empty($node->translation_source)) {
+          if (empty($node->translation_source->translation['trid'])) {
+            // Create new translation set, using nid from the source node.
+            $trid = $node->translation_source->nid;
+            $snid = $trid;
+            db_query("INSERT INTO {translation} (trid, nid, snid, status) VALUES (%d, %d, %d, %d)", $trid, $snid, $node->translation_source->nid, 0);
+          }
+          else {
+            // Add node to existing translation set.
+            $trid = $node->translation_source->translation['trid'];
+            $snid = $node->translation_source->nid;
+          }
+          db_query("INSERT INTO {translation} (trid, nid, snid, status) VALUES (%d, %d, %d, %d)", $trid, $node->nid, $snid, 0);
+        }
+        break;
+
+      case 'update':
+        if (isset($node->translation) && $node->translation && !empty($node->language)) {
+          if ($node->translation['trid']) {
+            // Update translation information.
+            db_query("UPDATE {translation} SET trid = %d, snid = %d, status = %d WHERE nid = %d", $node->translation['trid'], $node->translation['snid'], $node->translation['status'], $node->nid);
+            // If this is a source content, translations may need updating
+            if (!empty($node->translation['retranslate'])) {
+              db_query("UPDATE {translation} SET status = 1 WHERE trid = %d AND nid != %d", $node->translation['trid'], $node->nid);
+            }
+          }
+          else {
+            // The node may have been added to some existing translation set.
+            db_query("INSERT INTO {translation} (trid, nid, snid, status) VALUES (%d, %d, %d, %d)", $node->translation['trid'], $node->nid, $node->translation['snid'], $node->translation['status']);
+          }
+        }
+        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 (isset($node->translation['trid'])) {
+
+    $trid = $node->translation['trid'];
+    if (db_num_rows('SELECT * FROM {translations} WHERE trid = %d', $trid) < 2) {
+      // There will be only one node left in the set : we remove all
+      db_query('DELETE FROM {translation} WHERE trid = %d', $trid);
+    }
+    else {
+      db_query('DELETE FROM {translation} WHERE nid = %d', $node->nid);
+
+      // If the node being removed was the 'mother' of the translation set,
+      // we pick a new source - in order of preference :
+      // - one that had the 'mother' as source and is up-to-date
+      // - one that had the 'mother' as source and is not up-to-date
+      // - the 'oldest' node in the set (lowest nid)
+      if ($trid == $node->nid) {
+        $new_trid = db_result(db_query('SELECT nid FROM {translation} WHERE trid = %d AND snid = %d ORDER BY status ASC, nid ASC', $trid, $trid));
+        if (!$new_trid) {
+          $new_trid = db_result(db_query('SELECT nid FROM {translation} WHERE snid = %d ORDER BY nid ASC', $trid));
+        }
+        // We should have a new 'mother' by now.
+        if ($new_trid) {
+          db_query('UPDATE {translation} SET trid = %d WHERE trid = %d', $new_trid, $trid);
+        }
+      }
+
+      // We also have to set a new source for the nodes that were based on
+      // the node being removed - in order of preference :
+      // - the new 'mother' if we had to pick one
+      // - the source of the node being removed
+      //   (could be problematic if it was its own source, but that would mean
+      //   it was the 'mother', and we have a new one so we won't get there)
+      $new_snid = isset($new_trid) ? $new_trid : $node->translation['snid'];
+      $result = db_query('SELECT nid FROM {translation} WHERE snid = %d', $node->nid);
+      while ($row = db_fetch_object($result)) {
+        db_query('UPDATE {translation} SET snid = %d WHERE nid = %d', $new_snid, $row->nid);
+      }
+    }
+  }
+}
+
+/**
+ * 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.install
===================================================================
RCS file: modules/translation/translation.install
diff -N modules/translation/translation.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/translation/translation.install	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,36 @@
+<?php
+// $Id: translation.install,v 1.0 Exp $
+
+/**
+ * Implementation of hook_install().
+ */
+function translation_install() {
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+      db_query("CREATE TABLE {translation} (
+        trid int unsigned NOT NULL default '0',
+        nid int unsigned NOT NULL default '0',
+        snid int unsigned NOT NULL default '0',
+        status int NOT NULL default '0',  
+        PRIMARY KEY trid_nid (trid, nid)
+      ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
+      break;
+    case 'pgsql':
+      db_query("CREATE TABLE {translation} (
+        trid int_unsigned NOT NULL default '0',
+        nid int_unsigned NOT NULL default '0',
+        snid int_unsigned NOT NULL default '0',
+        status int NOT NULL default '0',       
+        PRIMARY KEY (trid, nid)
+      )");
+      break;
+  }
+}
+
+/**
+ * Implementation of hook_uninstall().
+ */
+function translation_uninstall() {
+  db_query('DROP TABLE {translation}');
+}
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
