You are here

translation.module in Internationalization 5

Same filename and directory in other branches
  1. 5.3 translation/translation.module
  2. 5.2 translation/translation.module

File

translation/translation.module
View source
<?php

/**
 * Internationalization (i18n) package.
 * 
 * Translation module: translation
 *
 * @author Jose A. Reyero, 2004, http://www.reyero.net
 *
 */

// Status for translations
define('TRANSLATION_STATUS_NONE', 0);
define('TRANSLATION_STATUS_SOURCE', 1);
define('TRANSLATION_STATUS_WORKING', 2);
define('TRANSLATION_STATUS_TRANSLATED', 3);
define('TRANSLATION_STATUS_UPDATED', 4);

/**
 * Implementation of hook_help().
 */
function translation_help($section = 'admin/help#translation') {
  switch ($section) {
    case 'admin/help#translation':
      $output = '<p>' . t('This module is part of i18n package and provides support for translation relationships.') . '</p>';
      $output .= '<p>' . t('The objects you can define translation relationships for are:') . '</p>';
      $output .= '<ul>';
      $output .= '<li>' . t('Nodes.') . '</li>';
      $output .= '<li>' . t('Taxonomy Terms') . '</li>';
      $output .= '</ul>';
      $output .= '<p>' . t('Additional features:') . '</p>';
      $output .= '<ul>';
      $output .= '<li>' . t('<i>Translations</i> block that looks like the language switcher provided by i18n module but also links to translations when available.') . '</li>';
      $output .= '<li>' . t('Basic translation workflow and administration page for content translation.') . '</li>';
      $output .= '<li>' . t('Links for node translations that can be displayed below each node, depending on module settings.') . '</li>';
      $output .= '</ul>';
      $output .= '<p>' . t('For more information please read the <a href="@i18n">on-line help pages</a>.', array(
        '@i18n' => 'http://drupal.org/node/31631',
      )) . '</p>';
      return $output;
      break;
    case 'admin/access#translation':
      $output = t('<h2>Translations</h2>');
      $output = t('<strong>translate nodes</strong> <p>This one, combined with create content permissions, will allow to create node translation</p>');
  }
  return $output;
}

/**
 * Implementation of hook_menu().
 */
function translation_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'translation/autocomplete',
      'title' => t('translation node autocomplete'),
      'callback' => 'translation_node_autocomplete',
      'access' => user_access('translate nodes'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/settings/i18n/translation',
      'title' => t('Translation'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'translation_admin_settings',
      ),
      'access' => user_access('administer site configuration'),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/content/translation',
      'title' => t('Translations'),
      'description' => t("Manage content translations."),
      'callback' => 'translation_admin_content',
      'position' => 'left',
      'access' => user_access('translate nodes'),
    );
    $items[] = array(
      'path' => 'admin/content/translation/overview',
      'title' => t('List'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => -10,
    );
  }
  else {
    if (arg(0) == 'node' && is_numeric(arg(1)) && variable_get('i18n_node_' . translation_get_node_type(arg(1)), 0)) {
      $access = user_access('translate nodes');
      $type = MENU_LOCAL_TASK;
      $items[] = array(
        'path' => 'node/' . arg(1) . '/translation',
        'title' => t('Translation'),
        'callback' => 'translation_node_page',
        'access' => $access,
        'type' => $type,
        'weight' => 3,
      );
    }
    if (arg(0) == 'admin' && arg(1) == 'content' && arg(2) == 'taxonomy' && is_numeric(arg(3))) {
      $items[] = array(
        'path' => 'admin/content/taxonomy/' . arg(3) . '/translation',
        'title' => t('Translation'),
        'callback' => 'translation_taxonomy_admin',
        'access' => user_access('administer taxonomy'),
        'type' => MENU_LOCAL_TASK,
      );
    }

    // Special redirections and rewrite conditions
    if (arg(0) == 'node' && arg(1) == 'add' && isset($_GET['translation']) && ($source_nid = $_GET['translation']) && isset($_GET['language']) && ($lang = $_GET['language']) && array_key_exists($lang, i18n_supported_languages())) {

      // Special redirection for product types. Check for product module because there may be 'product' node types created by cck
      if (arg(2) == 'product' && module_exists('product') && !arg(3) && is_numeric($source_nid) && ($ptype = db_result(db_query("SELECT ptype FROM {ec_product} WHERE nid = %d", $source_nid)))) {
        drupal_goto(url("node/add/product/{$ptype}", "translation={$source_nid}&language={$lang}", NULL, TRUE));
      }

      // Change rewrite conditions when translating node
      i18n_selection_mode('translation', db_escape_string($lang));
    }
  }
  return $items;
}

/**
 * Implementation of hook_perm
 */
function translation_perm() {
  return array(
    'translate nodes',
  );
}

/**
 * Form builder function: settings form
 */
function translation_admin_settings() {
  $form['i18n_translation_links'] = array(
    '#type' => 'radios',
    '#title' => t('Language Management'),
    '#default_value' => variable_get('i18n_translation_links', 0),
    '#options' => array(
      t('Interface language depends on content.'),
      t('Interface language is independent'),
    ),
    '#description' => t("Whether the whole page should switch language when clicking on a node translation or not."),
  );
  $form['i18n_translation_node_links'] = array(
    '#type' => 'radios',
    '#title' => t('Links to node translations'),
    '#default_value' => variable_get('i18n_translation_node_links', 0),
    '#options' => array(
      t('None.'),
      t('Main page only'),
      t('Teaser and Main page'),
    ),
    '#description' => t("Links from nodes to translated versions."),
  );
  $form['i18n_translation_workflow'] = array(
    '#type' => 'radios',
    '#title' => t('Translation workflow'),
    '#default_value' => variable_get('i18n_translation_workflow', 1),
    '#options' => array(
      t('Disabled'),
      t('Enabled'),
    ),
    '#description' => t("If enabled some worklow will be provided for content translation."),
  );
  return system_settings_form($form);
}

/**
 * Implementation of hook_block().
 * 
 * This is a simple language switcher which knows nothing about translations
 */
function translation_block($op = 'list', $delta = 0) {
  if ($op == 'list') {
    $blocks[0]['info'] = t('Translations');
  }
  elseif ($op == 'view') {
    $blocks['subject'] = t('Languages');
    $query = drupal_query_string_encode($_GET, array(
      'q',
    ));
    $blocks['content'] = theme('item_list', translation_get_links($_GET['q'], empty($query) ? NULL : $query));
  }
  return $blocks;
}

/**
 * Implementation of hook_form_alter().
 */
function translation_form_alter($form_id, &$form) {

  // Node add/edit form
  if (isset($form['type']) && $form['type']['#value'] . '_node_form' == $form_id && variable_get('i18n_node_' . $form['type']['#value'], 0)) {
    $node = $form['#node'];

    // Allow node to set it's own language list
    $languages = i18n_node_language_list($node);

    // Translation workflow, default
    if ($node->nid && $node->translation) {
      $translations = $node->translation;
    }
    elseif (isset($_GET['translation']) && user_access('translate nodes')) {

      // We are translating a node: node/add/type/translation/nid/lang
      $translation_nid = $_GET['translation'];
      $language = $_GET['language'];

      // Load the node to be translated and populate fields
      $trans = node_load($translation_nid);
      $form['i18n']['translation_nid'] = array(
        '#type' => 'hidden',
        '#value' => $translation_nid,
      );
      $form['i18n']['language']['#default_value'] = $language;

      // Do not allow language change
      $form['i18n']['language']['#disabled'] = TRUE;
      $form['i18n']['language']['#description'] = t('Language cannot be changed while creating a translation.');
      if ($trans->trid) {
        $form['i18n']['trid'] = array(
          '#type' => 'hidden',
          '#value' => $trans->trid,
        );
      }

      // Translations are taken from source node
      $translations = $trans->translation;
      $translations[$trans->language] = $trans;
    }

    // Display translations and restrict languages
    if ($translations) {

      // Unset invalid languages
      foreach (array_keys($translations) as $lang) {
        unset($form['i18n']['language']['#options'][$lang]);
      }

      // Add translation list
      $form['i18n']['#title'] = t('Language and translations');
      $form['i18n']['translations'] = array(
        '#type' => 'markup',
        '#value' => theme('translation_node_list', $translations, FALSE),
      );
    }

    // Translation workflow
    if (variable_get('i18n_translation_workflow', 1) && (user_access('translate nodes') || user_access('administer nodes'))) {
      $form['i18n']['i18n_status'] = array(
        '#type' => 'select',
        '#title' => t('Translation workflow'),
        '#options' => _translation_status(),
        '#description' => t('Use the translation workflow to keep track of content that needs translation.'),
      );
      if ($node->nid) {
        $form['i18n']['i18n_status']['#default_value'] = isset($node->i18n_status) ? $node->i18n_status : TRANSLATION_STATUS_NONE;
      }
      elseif (isset($trans)) {
        $form['i18n']['i18n_status']['#default_value'] = TRANSLATION_STATUS_WORKING;
      }
    }
    else {
      $form['i18n']['i18n_status'] = array(
        '#type' => 'value',
        '#value' => isset($node->i18n_status) ? $node->i18n_status : TRANSLATION_STATUS_NONE,
      );
    }

    // Clone files for original node ?
    if (isset($trans) && is_array($trans->files) && count($trans->files)) {
      $form['i18n']['translation_files'] = array(
        '#type' => 'fieldset',
        '#title' => t('Files from translated content'),
        '#tree' => TRUE,
        '#prefix' => '<div class="attachments">',
        '#suffix' => '</div>',
        '#theme' => 'upload_form_current',
        '#description' => t('You can remove the files for this translation or keep the original files and translate the description.'),
      );
      foreach ($trans->files as $key => $file) {
        $description = file_create_url(strpos($file->fid, 'upload') === false ? $file->filepath : file_create_filename($file->filename, file_create_path()));
        $description = "<small>" . check_plain($description) . "</small>";
        $form['i18n']['translation_files'][$key]['description'] = array(
          '#type' => 'textfield',
          '#default_value' => strlen($file->description) ? $file->description : $file->filename,
          '#maxlength' => 256,
          '#description' => $description,
        );
        $form['i18n']['translation_files'][$key]['size'] = array(
          '#type' => 'markup',
          '#value' => format_size($file->filesize),
        );
        $form['i18n']['translation_files'][$key]['remove'] = array(
          '#type' => 'checkbox',
          '#default_value' => 0,
        );
        $form['i18n']['translation_files'][$key]['list'] = array(
          '#type' => 'checkbox',
          '#default_value' => $file->list,
        );
        $form['i18n']['translation_files'][$key]['fid'] = array(
          '#type' => 'value',
          '#value' => $file->fid,
        );
      }
    }
  }
}

/**
 * Implementation of hook_nodeapi().
 * 
 * Delete case is now handled in i18n_nodeapi
 */
function translation_nodeapi(&$node, $op, $arg = 0) {
  if (variable_get("i18n_node_{$node->type}", 0)) {
    switch ($op) {
      case 'load':
        return array(
          'translation' => translation_node_get_translations(array(
            'nid' => $node->nid,
          ), FALSE),
        );
        break;
      case 'insert':
        if ($node->translation_nid) {

          // If not existing translation set, update both nodes. Otherwise trid is saved by i18n module
          if (!$node->trid) {
            $node->trid = db_next_id('{i18n_node}_trid');
            db_query("UPDATE {i18n_node} SET trid = %d WHERE nid=%d OR nid=%d", $node->trid, $node->nid, $node->translation_nid);
          }

          // Clone files for node attachments
          if (isset($node->translation_files)) {
            foreach ($node->translation_files as $fid => $file) {
              if (!$file['remove']) {

                // We are using revisions to have a file linked to different nodes, different descriptions
                db_query("INSERT INTO {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file['fid'], $node->vid, $file['list'], $file['description']);
              }
            }
          }
        }
        break;
      case 'prepare':

        // When creating a translation
        if (!$node->nid && isset($_GET['translation']) && ($nid = $_GET['translation']) && ($language = $_GET['language']) && user_access('translate nodes')) {

          // We are translating a node from a source node
          // Load the node to be translated and populate fields
          translation_node_prepare($node, $nid, $language);
        }
        break;
    }

    // Add nat integration
    if (module_exists('nat')) {
      translation_nodeapi_nat($node, $op);
    }
  }
}

/**
 * NAT (Node As Term) integration for node translations
 * 
 * This will run after i18n, translation and nat have done their job
 */
function translation_nodeapi_nat(&$node, $op) {
  $nat_config = variable_get('nat_config', array());

  // TO-DO: nat_get_terms has caching, look into it
  if (($op == 'insert' || $op == 'update') && isset($nat_config['types'][$node->type]) && !empty($nat_config['types'][$node->type]) && ($nat_terms = translation_nat_get_terms($node->nid))) {

    //drupal_set_message("DEBUG:translation_nodeapi_nat op=$op trid=$node->trid");
    $translations = $term_trids = array();

    // First of all, just update language for this node's terms.
    foreach ($nat_terms as $term) {
      db_query("UPDATE {term_data} SET language = '%s' WHERE tid = %d", $node->language, $term->tid);
      $translations[$term->vid][$node->language] = $term;
      if ($term->trid) {
        $term_trids[$term->vid] = $term->trid;
      }
    }

    // If the node is not in a translation set, there's nothing else to do
    if ($node->trid) {

      // Get node translations
      $node_translations = $node->trid ? translation_node_get_translations(array(
        'nid' => $node->nid,
      )) : array();

      // Now we build an array of the form $translations[vid][language] = $term,
      foreach ($node_translations as $trnode) {
        foreach (nat_get_terms($trnode->nid) as $term) {

          // If the term doesn't have a language we set that of the node it belongs to.
          // This should work for previously created terms.
          if (!$term->language) {
            $term->language = $trnode->language;
            db_query("UPDATE {term_data} SET language = '%s' WHERE tid = %d", $term->language, $term->tid);
          }
          $translations[$term->vid][$term->language] = $term;
          if ($term->trid) {

            // This will have some problem in the cases where terms for different nodes are in different
            // translation sets. That shouldn't happen though; so we force all of them in the same set.
            $term_trids[$term->vid] = $term->trid;
          }
        }
      }

      // Update the terms, assigning translations in the same vocabulary if possible
      foreach ($nat_terms as $term) {
        if (count($translations[$term->vid]) > 1) {
          $translation_set = $translations[$term->vid];
          $trid = isset($term_trids[$term->vid]) ? $term_trids[$term->vid] : 0;
          translation_taxonomy_save($translation_set, $trid);

          //drupal_set_message("DEBUG:translation_nodeapi_nat, trid=$trid data=".print_r($translation_set, TRUE));
        }
      }
    }
  }
}

/**
 * Like nat_get_terms() but without caching
 */
function translation_nat_get_terms($nid) {
  $return = array();
  $result = db_query("SELECT td.* FROM {nat} n INNER JOIN {term_data} td USING (tid) WHERE n.nid = %d", $nid);
  while ($term = db_fetch_object($result)) {
    $return[$term->tid] = $term;
  }
  return $return;
}

/**
 * Prepare node for translation
 */
function translation_node_prepare(&$node, $sourcenid, $language) {
  $node->translation_nid = $sourcenid;
  $node->language = $language;
  $source = $node->translation_source = node_load($node->translation_nid);

  // Taxonomy translation
  if (is_array($source->taxonomy)) {

    // Set translated taxonomy terms
    $node->taxonomy = array();
    foreach ($source->taxonomy as $tid => $term) {
      if ($term->language) {
        $translated_terms = translation_term_get_translations(array(
          'tid' => $tid,
        ));
        if ($translated_terms && ($newterm = $translated_terms[$node->language])) {
          $node->taxonomy[$newterm->tid] = $newterm;

          //drupal_set_message("DEBUG: Translated term $tid to ". $newterm->tid);
        }
      }
      else {

        // Term has no language. Should be ok
        $node->taxonomy[$tid] = $term;
      }
    }
  }

  // Book outlines, translating parent page if exists
  if ($source->parent && ($translations = translation_node_get_translations(array(
    'nid' => $source->parent,
  ))) && isset($translations[$language])) {
    $node->parent = $translations[$language]->nid;
  }

  // Translations are taken from source node
  $node->translation = $source->translation;
  $node->translation[$source->language] = $source;

  // Rest of fields. Unset some known ones not to be copied over
  unset($source->nid, $source->vid, $source->path, $source->language, $source->files);
  foreach ($source as $field => $value) {
    if (!isset($node->{$field})) {
      $node->{$field} = $value;
    }
  }
}

/**
 * Multilingual Nodes support
 */

/**
 * This is the callback for the tab 'translation' for nodes
 */
function translation_node_page() {
  $args = func_get_args();
  $op = $args[0];
  $nid = arg(1);
  $node = node_load($nid);

  // If node has no language, just warning message. Function returns here
  if (!$node->language) {
    form_set_error('language', t("You need to set a language before creating a translation."));
    drupal_goto("node/{$nid}/edit");
  }
  drupal_set_title(check_plain($node->title));
  $output = '';
  switch ($op) {
    case 'select':
      $output .= translation_node_overview($node);
      $output .= translation_node_select($node, $args[1]);
      break;
    default:
      $output .= translation_node_overview($node);
  }
  return $output;
}

/**
 * Form builder function
 */
function translation_node_form($node, $lang, $list) {
  $form['node'] = array(
    '#type' => 'value',
    '#value' => $node,
  );
  $form['language'] = array(
    '#type' => 'hidden',
    '#value' => $lang,
  );
  $form['source_nid'] = array(
    '#type' => 'hidden',
    '#value' => $node->nid,
  );
  $form['trid'] = array(
    '#type' => 'hidden',
    '#value' => $node->trid,
  );
  $languages = i18n_node_language_list($node);
  $form['select'] = array(
    '#type' => 'fieldset',
    '#title' => t('Select translation for %language', array(
      '%language' => $languages[$lang],
    )),
  );
  $form['select']['lookup'] = array(
    '#type' => 'textfield',
    '#title' => t('Search for node'),
    '#description' => t('Enter node id or title'),
    '#size' => 80,
    '#maxlength' => 254,
    '#autocomplete_path' => 'translation/autocomplete/' . $lang,
  );
  $form['select']['nodes']['nid'] = array(
    '#type' => 'radios',
    '#title' => t('Or select from the list'),
    '#default_value' => isset($node->translation[$lang]) ? $node->translation[$lang]->nid : '',
    '#options' => $list,
  );
  $form['select']['pager'] = array(
    '#value' => theme('pager'),
  );
  $form['select']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );
  return $form;
}

/**
* Menu callback: autocomplete for selecting node translations
*/
function translation_node_autocomplete($lang = NULL, $string = NULL) {
  $languages = i18n_supported_languages();
  if ($lang && $string && in_array($lang, array_keys($languages))) {
    $matches = array();
    $result = db_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n INNER JOIN {i18n_node} i ON n.nid = i.nid WHERE (LOWER(n.title) LIKE LOWER('%s%%') OR n.nid = %d) AND i.language = '%s'"), $string, $string, $lang);
    while ($nodelist = db_fetch_object($result)) {
      $title = check_plain($nodelist->title);
      $matches["{$nodelist->nid} - {$title}"] = "{$title} [nid={$nodelist->nid}]";
    }
    print drupal_to_js($matches);
  }
  exit;
}

/**
 * Menu callback: administration page for node translations
 */
function translation_admin_content() {
  $output = '';
  $defaults = array(
    'translation_language' => i18n_get_lang(),
    'source_status' => TRANSLATION_STATUS_SOURCE,
  );

  // Filter forms
  $output .= drupal_get_form('node_filter_form');
  $output .= drupal_get_form('translation_filter_form', $defaults);

  // Node listing depending on previous forms
  $output .= drupal_get_form('translation_admin_nodes');
  return $output;
}

/**
 * Form builder. Administrative form for node translations
 */
function translation_admin_nodes() {

  // First, translation filter because it may need parameters for join conditions
  $filter = translation_build_filter_query();
  $where = $filter['where'];
  $params = $filter['args'];
  $join = $filter['join'];
  $filter = node_build_filter_query();

  // Remove WHERE
  if ($filter['where']) {
    $where += array(
      str_replace('WHERE', '', $filter['where']),
    );
  }
  $params = array_merge($params, $filter['args']);
  $join .= $filter['join'];
  $sql = "SELECT n.nid, n.type, n.title, n.status, u.name, u.uid, i.language, i2.nid AS translation_nid, i2.status AS translation_status " . "FROM {node} n INNER JOIN {users} u ON n.uid = u.uid INNER JOIN {i18n_node} i ON n.nid = i.nid " . "LEFT JOIN {i18n_node} i2 ON i.trid = i2.trid AND 0 != i2.trid AND i.language != i2.language {$join}" . (count($where) ? ' WHERE ' . implode(' AND ', $where) : '');

  // Fetch data

  //drupal_set_message("DEBUG:query: $sql");

  //drupal_set_message("DEBUG:params: ".implode(', ', $params));
  i18n_selection_mode('off');
  $result = pager_query(db_rewrite_sql($sql), 50, 0, NULL, $params);
  i18n_selection_mode('reset');
  $languages = i18n_supported_languages();
  $translation_status = _translation_status();

  // Fetch language for translations
  $language = isset($_SESSION['translation_filter']['translation_language']) ? $_SESSION['translation_filter']['translation_language'] : i18n_get_lang();
  $destination = drupal_get_destination();
  while ($node = db_fetch_object($result)) {
    $nodes[$node->nid] = '';
    $form['language'][$node->nid] = array(
      '#value' => $languages[$node->language],
    );
    $form['title'][$node->nid] = array(
      '#value' => l($node->title, 'node/' . $node->nid) . ' ' . theme('mark', node_mark($node->nid, $node->changed)),
    );
    $form['name'][$node->nid] = array(
      '#value' => node_get_types('name', $node),
    );
    $form['username'][$node->nid] = array(
      '#value' => theme('username', $node),
    );
    $form['status'][$node->nid] = array(
      '#value' => $node->status ? t('published') : t('not published'),
    );
    if ($node->translation_nid) {
      $form['translation_status'][$node->nid] = array(
        '#value' => $translation_status[$node->translation_status],
      );
      $form['operations'][$node->nid] = array(
        '#value' => l(t('edit translation'), 'node/' . $node->translation_nid . '/edit', array(), $destination),
      );
    }
    else {
      $form['translation_status'][$node->nid] = array(
        '#value' => '--',
      );
      if ($language == $node->language) {
        $form['operations'][$node->nid] = array(
          '#value' => '--',
        );
      }
      else {
        $form['operations'][$node->nid] = array(
          '#value' => l(t('create translation'), 'node/add/' . $node->type, array(), "translation={$node->nid}&language={$language}") . ' | ' . l(t('select node'), "node/{$node->nid}/translation/select/{$language}", array(), $destination),
        );
      }
    }
  }

  /*
  $form['nodes'] = array('#type' => 'checkboxes', '#options' => $nodes);
  */
  $form['pager'] = array(
    '#value' => theme('pager', NULL, 50, 0),
  );
  return $form;
}

/**
 * Build query for node administration filters based on session.
 */
function translation_build_filter_query() {

  // Build query
  $where = $args = array();
  $join = '';

  // This will produce an empty join
  if (!is_array($_SESSION['translation_filter'])) {
    $_SESSION['translation_filter'] = array();
  }
  foreach ($_SESSION['translation_filter'] as $type => $value) {
    switch ($type) {
      case 'source_language':
        $where[] = "i.language = '%s'";
        $args[] = $value;
        break;
      case 'translation_language':
        $join .= " AND i2.language ='" . db_escape_string($value) . "' ";
        break;
      case 'source_status':
        $where[] = "i.status = %d";
        $args[] = $value;
        break;
      case 'translation_status':
        $join .= " AND i2.status = " . db_escape_string($value);
        break;
    }
  }
  return array(
    'where' => $where,
    'join' => $join,
    'args' => $args,
  );
}

/**
 * Returns form for translation administration filters.
 */
function translation_filter_form($defaults = array()) {
  $session =& $_SESSION['translation_filter'];
  $session = is_array($session) ? $session : $defaults;

  // Save defaults for form reset
  $form['_defaults'] = array(
    '#type' => 'value',
    '#value' => $defaults,
  );
  $form['filters'] = array(
    '#type' => 'fieldset',
    '#title' => t('And translation conditions are'),
  );
  $languages = i18n_supported_languages();

  // Translation and language conditions
  $form['filters']['source_language'] = array(
    '#type' => 'select',
    '#title' => t('source language'),
    '#options' => array(
      '' => '',
    ) + $languages,
    '#default_value' => $session['source_language'],
  );
  $form['filters']['source_status'] = array(
    '#type' => 'select',
    '#title' => t('source status'),
    '#options' => array(
      '' => '',
    ) + _translation_status(),
    '#default_value' => $session['source_status'],
  );
  $form['filters']['translation_language'] = array(
    '#type' => 'select',
    '#title' => t('translation language'),
    '#options' => array(
      '' => '',
    ) + $languages,
    '#default_value' => $session['translation_language'],
  );
  $form['filters']['translation_status'] = array(
    '#type' => 'select',
    '#title' => t('translation status'),
    '#options' => array(
      '' => '',
    ) + _translation_status(),
    '#default_value' => $session['translation_status'],
  );
  $form['filters']['buttons']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Filter'),
  );
  if (count($session)) {
    $form['filters']['buttons']['reset'] = array(
      '#type' => 'submit',
      '#value' => t('Reset'),
    );
  }
  return $form;
}
function theme_translation_filter_form($form) {
  $form['filters']['source_language']['#prefix'] = '<table><tr><td>';
  $form['filters']['source_status']['#suffix'] = '</td><td>';
  $form['filters']['buttons']['#prefix'] = '</td><td>';
  $form['filters']['buttons']['#suffix'] = '</td></tr></table>';
  return drupal_render($form);
}

/**
 * Builds translation admin filters
 */
function translation_filter_form_submit($form_id, $form_values) {
  $op = $_POST['op'];
  $session =& $_SESSION['translation_filter'];
  switch ($op) {
    case t('Filter'):
      $session = array();
      foreach ($form_values as $name => $value) {
        if ($value) {
          $session[$name] = $value;
        }
      }
      break;
    case t('Reset'):
      $session = $form_values['_defaults'];
      break;
  }
}

/**
 * Theme node administration overview.
 */
function theme_translation_admin_nodes($form) {

  // Overview table:
  $header = array(
    t('Language'),
    t('Title'),
    t('Type'),
    t('Author'),
    t('Status'),
    t('Translation status'),
    t('Operations'),
  );
  $output .= drupal_render($form['options']);
  if (isset($form['title']) && is_array($form['title'])) {
    foreach (element_children($form['title']) as $key) {
      $row = array();
      $row[] = drupal_render($form['language'][$key]);
      $row[] = drupal_render($form['title'][$key]);
      $row[] = drupal_render($form['name'][$key]);
      $row[] = drupal_render($form['username'][$key]);
      $row[] = drupal_render($form['status'][$key]);
      $row[] = drupal_render($form['translation_status'][$key]);
      $row[] = drupal_render($form['operations'][$key]);
      $rows[] = $row;
    }
  }
  else {
    $rows[] = array(
      array(
        'data' => t('No posts available.'),
        'colspan' => '6',
      ),
    );
  }
  $output .= theme('table', $header, $rows);
  if ($form['pager']['#value']) {
    $output .= drupal_render($form['pager']);
  }
  $output .= drupal_render($form);
  return $output;
}

/**
 * Shows overview of current translations plus remove button
 * @param $node
 *   Node to show translations for
 * @param $languages
 *   Alternate list of languages
 */
function translation_node_overview($node) {
  $languages = i18n_node_language_list($node);
  unset($languages[$node->language]);

  // $output = t('<h2>Current translations</h2>');
  $header = array(
    t('Language'),
    t('Title'),
    t('Status'),
    t('Options'),
  );

  // Special links for product nodes
  $createlink = $node->type == 'product' ? "node/add/{$node->type}/{$node->ptype}" : "node/add/{$node->type}";
  foreach ($languages as $lang => $langname) {
    $options = array();
    if (isset($node->translation[$lang])) {
      $trnode = $node->translation[$lang];
      $title = l($trnode->title, 'node/' . $trnode->nid);
      $status = $trnode->status ? t('Published') : t('Not published');
    }
    else {
      $title = t('Not translated');
      $options[] = l(t('create translation'), $createlink, array(), "translation={$node->nid}&language={$lang}");
      $status = '--';
    }
    $options[] = l(t('select node'), "node/{$node->nid}/translation/select/{$lang}");
    $rows[] = array(
      $langname,
      $title,
      $status,
      implode(" | ", $options),
    );
  }
  $output .= theme('table', $header, $rows);
  if ($node->trid) {
    $output .= drupal_get_form('translation_node_remove', $node);
  }
  return theme('box', t('Current translations'), $output);
}

/**
 * Form to remove node from translation set
 */
function translation_node_remove($node) {
  $form['remove'] = array(
    '#type' => 'fieldset',
    '#title' => t('Remove from translation'),
    '#description' => t('This will just unlink the node from this translation set.'),
  );
  $form['remove']['node'] = array(
    '#type' => 'value',
    '#value' => $node,
  );
  $form['remove']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Remove'),
  );
  return $form;
}

/**
 * Form submit. Remove from translation set without confirmation.
 */
function translation_node_remove_submit($form_id, $form_values) {
  $node = $form_values['node'];
  db_query("UPDATE {i18n_node} SET trid = 0 WHERE nid = %d", $node->nid);
  drupal_set_message(t("The node has been removed from the translation set"));
  drupal_goto("node/{$node->nid}/translation");
}

/**
 * Form to select a translation from existing nodes
 */
function translation_node_select($node, $lang) {
  $languages = i18n_supported_languages();

  // Disable i18n rewrite. Order by trid to show first nodes with no translation
  i18n_selection_mode('off');
  $result = pager_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n INNER JOIN {i18n_node} i ON n.nid = i.nid WHERE i.language = '%s' ORDER BY i.trid"), 40, 0, NULL, $lang);
  i18n_selection_mode('reset');
  while ($trnode = db_fetch_object($result)) {
    $list[$trnode->nid] = l($trnode->title, "node/{$trnode->nid}");
  }
  if ($list) {
    return drupal_get_form('translation_node_form', $node, $lang, $list);
  }
  else {
    return t("<p>No nodes available in %language</p>", array(
      '%language' => $languages[$lang],
    ));
  }
}

/**
 * Process translation node form
 */
function translation_node_form_submit($form_id, $form_values) {
  $op = $_POST['op'];
  $source_nid = $form_values['source_nid'];
  $language = $form_values['language'];
  if ($form_values['nid']) {
    $nid = $form_values['nid'];
  }
  if ($form_values['lookup'] && ($number = intval($form_values['lookup']))) {
    $nid = $number;
  }
  if ($source_nid && $language && $nid && ($node = node_load($nid)) && $node->language == $language) {
    if ($trid = $form_values['trid']) {

      // Delete old translations
      db_query("UPDATE {i18n_node} SET trid = 0 WHERE trid = %d AND language = '%s'", $trid, $language);
    }
    else {
      $trid = db_next_id('{i18n_node}_trid');
    }
    db_query("UPDATE {i18n_node} SET trid = %d WHERE nid=%d OR nid=%d", $trid, $source_nid, $nid);
    drupal_set_message(t('The translation has been saved'));
  }
}

/**
 * Implementation of hook taxonomy.
 */
function translation_taxonomy($op, $type = NULL, $edit = NULL) {
  switch ("{$type}/{$op}") {
    case 'term/insert':
    case 'term/update':
      if (!$edit['language'] && $edit['trid']) {

        // Removed language, remove trid
        db_query('UPDATE {term_data} SET trid = 0 WHERE tid = %d', $edit['tid']);
        if (db_affected_rows()) {
          drupal_set_message(t('Removed translation information from term'));
        }
      }
      break;
  }
}

/**
 * Implementation of hook_link().
 */
function translation_link($type, $node = NULL, $teaser = FALSE) {
  $languages = i18n_supported_languages();
  $links = array();
  if ($type == 'node' && variable_get('i18n_translation_node_links', 0) > ($teaser ? 1 : 0) && $node->translation) {
    foreach ($node->translation as $lang => $trnode) {

      // Add node link if published
      if ($trnode->status) {

        // If language is not enabled, we'll use current language for the prefix
        $baselang = variable_get('i18n_translation_links', 0) || !isset($languages[$lang]) ? i18n_get_lang() : $lang;
        $links['translation-' . $lang] = theme('translation_node_link', $trnode, $lang, $baselang);
      }
    }
  }
  return $links;
}

/**
 * Returns a list for terms for vocabulary, language
 */
function translation_vocabulary_get_terms($vid, $lang, $status = 'all') {
  switch ($status) {
    case 'translated':
      $andsql = ' AND trid > 0';
      break;
    case 'untranslated':
      $andsql = ' AND trid = 0';
      break;
    default:
      $andsql = '';
  }
  $result = db_query("SELECT * FROM {term_data} WHERE vid=%d AND language='%s' {$andsql}", $vid, $lang);
  $list = array();
  while ($term = db_fetch_object($result)) {
    $list[$term->tid] = $term->name;
  }
  return $list;
}

/**
 *	Get translations
 *
 * @param $params
 *   array of parameters
 * @param $getall
 *   TRUE to get the also node itself
 */
function translation_node_get_translations($params, $getall = TRUE) {
  foreach ($params as $field => $value) {
    $conds[] = "b.{$field} = '%s'";
    $values[] = $value;
  }
  if (!$getall) {

    // If not all, a parameter must be nid
    $conds[] = "n.nid != %d";
    $values[] = $params['nid'];
  }
  $conds[] = "b.trid != 0";
  $sql = 'SELECT n.nid, n.title, n.status, a.language FROM {node} n INNER JOIN {i18n_node} a ON n.nid = a.nid INNER JOIN {i18n_node} b ON a.trid = b.trid WHERE ' . implode(' AND ', $conds);
  i18n_selection_mode('off');
  $result = db_query(db_rewrite_sql($sql), $values);
  i18n_selection_mode('reset');
  $items = array();
  while ($node = db_fetch_object($result)) {
    $items[$node->language] = $node;
  }
  return $items;
}

/**
 * Returns node type for nid
 */
function translation_get_node_type($nid) {
  return db_result(db_query('SELECT type FROM {node} WHERE nid=%d', $nid));
}

/**
 * Multilingual Taxonomy
 *
 */

/**
 * This is the callback for taxonomy translations
 * 
 * Gets the urls:
 * 	 admin/content/taxonomy/[vid]/translation/edit/[trid]
 *   admin/content/taxonomy/[vid]/translation/new
 */
function translation_taxonomy_admin($op = NULL, $trid = NULL) {
  $vid = arg(3);
  $op = arg(5);
  $trid = arg(6);
  $output = '';
  switch ($op) {
    case 'edit':
    case 'new':
      drupal_set_title(t('Edit term translations'));
      $output .= drupal_get_form('translation_taxonomy_term_form', $vid, $trid);
      break;
    default:
      $output .= translation_taxonomy_overview($vid);
  }
  return $output;
}

/**
 * Generate a tabular listing of translations for vocabularies.
 */
function translation_taxonomy_overview($vid) {
  $vocabulary = taxonomy_get_vocabulary($vid);
  drupal_set_title(check_plain($vocabulary->name));
  $languages = i18n_supported_languages();
  $header = array_merge($languages, array(
    t('Operations'),
  ));
  $links = array();
  $types = array();

  // Get terms/translations for this vocab
  $result = db_query('SELECT * FROM {term_data} t WHERE vid=%d', $vocabulary->vid);
  $terms = array();
  while ($term = db_fetch_object($result)) {
    if ($term->trid && $term->language) {
      $terms[$term->trid][$term->language] = $term;
    }
  }

  // Reorder data for rows and languages
  foreach ($terms as $trid => $terms) {
    $thisrow = array();
    foreach ($languages as $lang => $name) {
      if (array_key_exists($lang, $terms)) {
        $thisrow[] = l($terms[$lang]->name, 'taxonomy/term/' . $terms[$lang]->tid);
      }
      else {
        $thisrow[] = '--';
      }
    }
    $thisrow[] = l(t('edit'), "admin/content/taxonomy/{$vid}/translation/edit/{$trid}");
    $rows[] = $thisrow;
  }
  $output .= theme('table', $header, $rows);
  $output .= l(t('new translation'), "admin/content/taxonomy/{$vid}/translation/new");
  return $output;
}

/**
 * Produces a vocabulary translation form
 */
function translation_taxonomy_term_form($vid, $trid = NULL) {
  $languages = i18n_supported_languages();
  if (!$trid) {
    $translations = array();
  }
  else {
    $form['trid'] = array(
      '#type' => 'hidden',
      '#value' => $trid,
    );
    $translations = translation_term_get_translations(array(
      'trid' => $trid,
    ));
  }

  //var_dump($translations);
  $vocabulary = taxonomy_get_vocabulary($vid);

  // List of terms for languages
  foreach ($languages as $lang => $langname) {
    $current = isset($translations[$lang]) ? $translations[$lang]->tid : '';
    $list = translation_vocabulary_get_terms($vid, $lang, 'all');
    $list[''] = '--';
    $form[$lang] = array(
      '#type' => 'fieldset',
      '#tree' => TRUE,
    );
    $form[$lang]['tid'] = array(
      '#type' => 'select',
      '#title' => $langname,
      '#default_value' => $current,
      '#options' => $list,
    );
    $form[$lang]['old'] = array(
      '#type' => 'hidden',
      '#value' => $current,
    );
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );
  $form['#redirect'] = 'admin/content/taxonomy/' . $vid . '/translation';
  return $form;
}

/**
 * Form callback: Process vocabulary translation form
 */
function translation_taxonomy_term_form_submit($form_id, $form_values) {
  translation_taxonomy_save($form_values, $form_values['trid']);
  drupal_set_message(t('Term translations have been updated'));
}

/**
 * Save taxonomy term translations
 * 
 * @param $terms
 *   Array of terms indexed by language
 * @param $trid
 *   Optional translation set id
 */
function translation_taxonomy_save($terms, $trid = 0) {

  // Delete old translations for this trid
  if ($trid) {
    db_query("UPDATE {term_data} SET trid = 0 WHERE trid= %d", $trid);
  }

  // Now pick up all the tids in an array
  $translations = array();
  foreach (i18n_supported_languages() as $lang => $name) {
    if (isset($terms[$lang]) && ($term = (array) $terms[$lang]) && ($tid = $term['tid'])) {
      $translations[$lang] = $tid;
    }
  }

  // Now set a translation set with all these terms.
  if (count($translations)) {
    $trid = is_numeric($trid) && $trid ? $trid : db_next_id('{term_data}_trid');
    db_query('UPDATE {term_data} SET trid = %d WHERE tid IN(%s)', $trid, implode(',', $translations));
  }
}

/**
 * Converts a list of arrays to an array of the form keyfield => namefield
 */
function translation_array2list($data, $keyfield, $namefield = 'name') {
  foreach ($data as $key => $value) {
    if (is_array($data)) {
      $list[$value[$keyfield]] = $value[$namefield];
    }
    else {
      $list[$value->{$keyfield}] = $value->{$namefield};
    }
  }
  return $list;
}

/**
 * Get term translations
 * 
 * @return
 *   An array of the from lang => Term
 */
function translation_term_get_translations($params, $getall = TRUE) {
  foreach ($params as $field => $value) {
    $conds[] = "i.{$field} = '%s'";
    $values[] = $value;
  }
  if (!$getall) {

    // If not all, a parameter must be tid
    $conds[] = "t.tid != %d";
    $values[] = $params['tid'];
  }
  $conds[] = "t.trid != 0";
  $sql = 'SELECT t.* FROM {term_data} t INNER JOIN {term_data} i ON t.trid = i.trid WHERE ' . implode(' AND ', $conds);
  $result = db_query($sql, $values);
  $items = array();
  while ($data = db_fetch_object($result)) {
    $items[$data->language] = $data;
  }
  return $items;
}

/**
 * Produces url of translated page
 */
function translation_url($url, $lang) {
  global $i18n_langpath;

  // If !url get from original request
  if (!$url) {
    $url = _i18n_get_original_path();
  }

  // If url has lang_prefix, remove it
  i18n_get_lang_prefix($url, true);

  // are we looking at a node?
  if (preg_match("/^(node\\/)([0-9]*)(.*)\$/", $url, $matches)) {
    if ($nid = translation_node_nid($matches[2], $lang)) {
      $url = "node/{$nid}" . $matches[3];
    }
  }
  elseif (preg_match("/^(taxonomy\\/term\\/)([^\\/]*)(.*)\$/", $url, $matches)) {

    //or at a taxonomy-listing?
    if ($str_tids = translation_taxonomy_tids($matches[2], $lang)) {
      $url = "taxonomy/term/{$str_tids}" . $matches[3];
    }
  }
  return $url;
}

/**
 * Get translated node's nid
 * 
 * @param $nid
 *   Node nid to search for translation
 * @param $language
 *   Language to search for the translation, defaults to current language
 * @param $default
 *   Value that will be returned if no translation is found
 * @return
 *   Translated node nid if exists, or $default
 */
function translation_node_nid($nid, $language = NULL, $default = NULL) {
  $translation = db_result(db_query("SELECT n.nid FROM {i18n_node} n INNER JOIN {i18n_node} a ON n.trid = a.trid AND n.nid != a.nid WHERE a.nid = %d AND n.language = '%s' AND n.trid", $nid, $language ? $language : i18n_get_lang()));
  return $translation ? $translation : $default;
}

/**
 * Get translated term's tid
 * 
 * @param $tid
 *   Node nid to search for translation
 * @param $language
 *   Language to search for the translation, defaults to current language
 * @param $default
 *   Value that will be returned if no translation is found
 * @return
 *   Translated term tid if exists, or $default
 */
function translation_term_tid($tid, $language = NULL, $default = NULL) {
  $translation = db_result(db_query("SELECT t.tid FROM {term_data} t INNER JOIN {term_data} a ON t.trid = a.trid AND t.tid != a.tid WHERE a.tid = %d AND t.language = '%s' AND t.trid", $tid, $language ? $language : i18n_get_lang()));
  return $translation ? $translation : $default;
}

/**
 *  Returns an url for the translated taxonomy-page, if exists 
 */
function translation_taxonomy_tids($str_tids, $lang) {
  if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str_tids)) {
    $separator = '+';

    // The '+' character in a query string may be parsed as ' '.
    $tids = preg_split('/[+ ]/', $str_tids);
  }
  else {
    if (preg_match('/^([0-9]+,)*[0-9]+$/', $str_tids)) {
      $separator = ',';
      $tids = explode(',', $str_tids);
    }
    else {
      return;
    }
  }
  $translated_tids = array();
  foreach ($tids as $tid) {
    if ($translated_tid = translation_term_tid($tid, $lang)) {
      $translated_tids[] = $translated_tid;
    }
  }
  return implode($separator, $translated_tids);
}

/**
 * Returns an array of links for all languages
 * 
 * @param $path
 *   Drupal internal path
 * @param $query
 *   Query string
 * @param $names
 *   Names to use for the links. Defaults to native language names
 */
function translation_get_links($path = '', $query = NULL, $names = NULL) {
  $current = i18n_get_lang();
  $names = $names ? $names : i18n_languages('native');
  foreach (array_keys(i18n_supported_languages()) as $lang) {
    $url = translation_url($path, $lang);
    $links[] = theme('i18n_link', $names[$lang], i18n_path($url, $lang), $lang, $query);
  }
  return $links;
}

/**
 * Return status for translations workflow
 */
function _translation_status() {
  return array(
    TRANSLATION_STATUS_NONE => t('None'),
    TRANSLATION_STATUS_SOURCE => t('Source content (to be translated)'),
    TRANSLATION_STATUS_WORKING => t('Translation in progress'),
    TRANSLATION_STATUS_TRANSLATED => t('Translated content'),
    TRANSLATION_STATUS_UPDATED => t('Source updated (to update translation)'),
  );
}

/**
 * Theme a link to node translation
 * 
 * Since Drupal 5, returns array instead of html
 */
function theme_translation_node_link($node, $lang, $baselang = NULL, $title = FALSE) {
  $baselang = $baselang ? $baselang : $lang;
  if ($title) {
    $name = $node->title;
  }
  else {
    $languages = i18n_languages('native');
    $name = $languages[$lang];
  }
  return array(
    'title' => $name,
    'href' => i18n_path('node/' . $node->nid, $baselang),
  );
}

/**
 * Theme a link for a translation
 */
function theme_translation_link($text, $target, $lang, $separator = '&nbsp;') {
  return theme('i18n_link', $text, $target, $lang, $separator);
}

/**
 * Theme list of node translations
 */
function theme_translation_node_list($list) {
  $header = array(
    t('Language'),
    t('Title'),
  );

  // Use full language list with English names, then localize
  $languages = i18n_languages('name');
  foreach ($list as $lang => $node) {
    $rows[] = array(
      $languages[$lang],
      l($node->title, 'node/' . $node->nid),
    );
  }
  return theme('table', $header, $rows);
}

Functions

Namesort descending Description
theme_translation_admin_nodes Theme node administration overview.
theme_translation_filter_form
theme_translation_link Theme a link for a translation
theme_translation_node_link Theme a link to node translation
theme_translation_node_list Theme list of node translations
translation_admin_content Menu callback: administration page for node translations
translation_admin_nodes Form builder. Administrative form for node translations
translation_admin_settings Form builder function: settings form
translation_array2list Converts a list of arrays to an array of the form keyfield => namefield
translation_block Implementation of hook_block().
translation_build_filter_query Build query for node administration filters based on session.
translation_filter_form Returns form for translation administration filters.
translation_filter_form_submit Builds translation admin filters
translation_form_alter Implementation of hook_form_alter().
translation_get_links Returns an array of links for all languages
translation_get_node_type Returns node type for nid
translation_help Implementation of hook_help().
translation_link Implementation of hook_link().
translation_menu Implementation of hook_menu().
translation_nat_get_terms Like nat_get_terms() but without caching
translation_nodeapi Implementation of hook_nodeapi().
translation_nodeapi_nat NAT (Node As Term) integration for node translations
translation_node_autocomplete Menu callback: autocomplete for selecting node translations
translation_node_form Form builder function
translation_node_form_submit Process translation node form
translation_node_get_translations * Get translations
translation_node_nid Get translated node's nid
translation_node_overview Shows overview of current translations plus remove button
translation_node_page This is the callback for the tab 'translation' for nodes
translation_node_prepare Prepare node for translation
translation_node_remove Form to remove node from translation set
translation_node_remove_submit Form submit. Remove from translation set without confirmation.
translation_node_select Form to select a translation from existing nodes
translation_perm Implementation of hook_perm
translation_taxonomy Implementation of hook taxonomy.
translation_taxonomy_admin This is the callback for taxonomy translations
translation_taxonomy_overview Generate a tabular listing of translations for vocabularies.
translation_taxonomy_save Save taxonomy term translations
translation_taxonomy_term_form Produces a vocabulary translation form
translation_taxonomy_term_form_submit Form callback: Process vocabulary translation form
translation_taxonomy_tids Returns an url for the translated taxonomy-page, if exists
translation_term_get_translations Get term translations
translation_term_tid Get translated term's tid
translation_url Produces url of translated page
translation_vocabulary_get_terms Returns a list for terms for vocabulary, language
_translation_status Return status for translations workflow

Constants