'.t('This module is part of i18n package and provides support for translation relationships.').'
'; $output .= ''.t('The objects you can define translation relationships for are:').'
'; $output .= ''.t('Additional features:').'
'; $output .= ''.t('For more information please read the on-line help pages.', array('@i18n' =>'http://drupal.org/node/31631')) .'
'; return $output; break; case 'admin/access#translation': $output = t('This one, combined with create content permissions, will allow to create node translation
'); } 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)) && ($node = node_load(arg(1))) && variable_get('i18n_node_'.translation_get_node_type(arg(1)), 0)) { $access = translation_access($node) && node_access('view', $node); $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', 'translate own nodes'); } /** * Implementation of hook_access(). */ function translation_access($node) { global $user; return user_access('translate nodes') || $node->uid == $user->uid && user_access('translate own 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 for Content'); } 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($node->translation_source) && translation_access($node->translation_source)) { // We are translating a node: node/add/$type?translation=$nid&language=$language $translation_nid = $node->translation_nid; $language = $node->language; // Get the node to be translated and populate fields $trans = $node->translation_source; $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' => ' ', '#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 = "". check_plain($description) .""; $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 (empty($node->nid) && isset($_GET['translation']) && is_numeric($_GET['translation']) && !empty($_GET['language'])) { // We are translating a node from a source node // Load the node to be translated and populate fields translation_node_prepare($node, $_GET['translation'], $_GET['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) { // Validate source and language. $source = node_load($sourcenid); if (empty($source) || !translation_access($source) || !node_access('view', $source)) { // Source node not found or no access to view. return; } $language_list = i18n_node_language_list($node); if (!isset($language_list[$language]) || ($source->language == $language)) { // If not supported language, or same language as source node, break. return; } $node->translation_source = drupal_clone($source); $node->translation_nid = $sourcenid; $node->language = $language; // 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; // Only copy the body if the translator can access the format. if (filter_access($source->format)) { $node->body = $source->body; } // Rest of fields. Unset some known ones not to be copied over unset($source->nid, $source->vid, $source->path, $source->language, $source->files, $source->body); 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']; // Now we add in conditions from the node admin filter subform $filter = node_build_filter_query(); // Remove WHERE and merge all conditions. The WHERE part from node filter was a string with 'WHERE..' if (!empty($filter['where'])) { $where += array_merge($where, array(str_replace('WHERE', '', $filter['where']))); } if (!empty($filter['args'])) { $params = array_merge($params, $filter['args']); } if (!empty($filter['join'])) { $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. Temporarily disable language query rewriting before 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 { $prefix = variable_get('i18n_translation_links', 0) ? i18n_get_lang() : $language; $form['operations'][$node->nid] = array('#value' => l(t('create translation'), $prefix.'/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'), '#theme' => 'translation_filter_form_layout', ); $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 (!empty($session)) { $form['filters']['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset')); } return $form; } // Quick and dirty way to get some form layout function theme_translation_filter_form_layout($form) { $output = '' . drupal_render($form['source_language']) . ' | '; $output .= '' . drupal_render($form['translation_language']) . ' | '; $output .= '
' . drupal_render($form['source_status']) . ' | '; $output .= '' . drupal_render($form['translation_status']) . ' | '; $output .= '