Code coverage for /20080809/modules/translation/translation.module

Line #Times calledCode
1
<?php
2
// $Id: translation.module,v 1.28 2008/07/22 20:06:02 dries Exp $
3
4
/**
5
 * @file
6
 *   Manages content translations.
7
 *
8
 *   Translations are managed in sets of posts, which represent the same
9
 *   information in different languages. Only content types for which the
10
 *   administrator explicitly enabled translations could have translations
11
 *   associated. Translations are managed in sets with exactly one source
12
 *   post per set. The source post is used to translate to different
13
 *   languages, so if the source post is significantly updated, the
14
 *   editor can decide to mark all translations outdated.
15
 *
16
 *   The node table stores the values used by this module:
17
 *    - 'tnid' is the translation set id, which equals the node id
18
 *      of the source post.
19
 *    - 'translate' is a flag, either indicating that the translation
20
 *      is up to date (0) or needs to be updated (1).
21
 */
22
23
/**
24
 * Identifies a content type which has translation support enabled.
25
 */
2637
define('TRANSLATION_ENABLED', 2);
27
28
/**
29
 * Implementation of hook_help().
30
 */
3137
function translation_help($path, $arg) {
32
  switch ($path) {
3325
    case 'admin/help#translation':
341
      $output = '<p>' . t('The content translation module allows content to
be translated into different languages. Working with the <a
href="@locale">locale module</a> (which manages enabled languages and
provides translation for the site interface), the content translation
module is key to creating and maintaining translated site content.',
array('@locale' => url('admin/help/locale'))) . '</p>';
351
      $output .= '<p>' . t('Configuring content translation and
translation-enabled content types:') . '</p>';
361
      $output .= '<ul><li>' . t('Assign the <em>translate content</em>
permission to the appropriate user roles at the <a
href="@permissions">Permissions configuration page</a>.',
array('@permissions' => url('admin/user/permissions'))) . '</li>';
371
      $output .= '<li>' . t('Add and enable desired languages at the <a
href="@languages">Languages configuration page</a>.', array('@languages' =>
url('admin/settings/language'))) . '</li>';
381
      $output .= '<li>' . t('Determine which <a
href="@content-types">content types</a> should support translation
features. To enable translation support for a content type, edit the type
and at the <em>Multilingual support</em> drop down, select <em>Enabled,
with translation</em>. (<em>Multilingual support</em> is located within
<em>Workflow settings</em>.) Be sure to save each content type after
enabling multilingual support.', array('@content-types' =>
url('admin/build/types'))) . '</li></ul>';
391
      $output .= '<p>' . t('Working with translation-enabled content
types:') . '</p>';
401
      $output .= '<ul><li>' . t('Use the <em>Language</em> drop down to
select the appropriate language when creating or editing posts.') .
'</li>';
411
      $output .= '<li>' . t('Provide new or edit current translations for
existing posts via the <em>Translation</em> tab. Only visible while viewing
a post as a user with the <em>translate content</em> permission, this tab
allows translations to be added or edited using a specialized editing form
that also displays the content being translated.') . '</li>';
421
      $output .= '<li>' . t('Update translations as needed, so that they
accurately reflect changes in the content of the original post. The
translation status flag provides a simple method for tracking outdated
translations. After editing a post, for example, select the <em>Flag
translations as outdated</em> check box to mark all of its translations as
outdated and in need of revision. Individual translations may be marked for
revision by selecting the <em>This translation needs to be updated</em>
check box on the translation editing form.') . '</li>';
431
      $output .= '<li>' . t('The <a href="@content-node">Content management
administration page</a> displays the language of each post, and also allows
filtering by language or translation status.', array('@content-node' =>
url('admin/content/node'))) . '</li></ul>';
441
      $output .= '<p>' . t('Use the <a href="@blocks">language switcher
block</a> provided by locale module to allow users to select a language. If
available, both the site interface and site content are presented in the
language selected.', array('@blocks' => url('admin/build/block'))) .
'</p>';
451
      $output .= '<p>' . t('For more information, see the online handbook
entry for <a href="@translation">Translation module</a>.',
array('@translation' => 'http://drupal.org/handbook/modules/translation/'))
. '</p>';
461
      return $output;
4725
    case 'node/%/translate':
481
      $output = '<p>' . t('Translations of a piece of content are managed
with translation sets. Each translation set has one source post and any
number of translations in any of the <a href="!languages">enabled
languages</a>. All translations are tracked to be up to date or outdated
based on whether the source post was modified significantly.',
array('!languages' => url('admin/settings/language'))) . '</p>';
491
      return $output;
500
  }
5124
}
52
53
/**
54
 * Implementation of hook_menu().
55
 */
5637
function translation_menu() {
573
  $items = array();
583
  $items['node/%node/translate'] = array(
593
    'title' => 'Translate',
603
    'page callback' => 'translation_node_overview',
613
    'page arguments' => array(1),
623
    'access callback' => '_translation_tab_access',
633
    'access arguments' => array(1),
643
    'type' => MENU_LOCAL_TASK,
653
    'weight' => 2,
66
  );
673
  return $items;
680
}
69
70
/**
71
 * Menu access callback.
72
 *
73
 * Only display translation tab for node types, which have translation
enabled
74
 * and where the current node is not language neutral (which should span
75
 * all languages).
76
 */
7737
function _translation_tab_access($node) {
787
  if (!empty($node->language) && translation_supported_type($node->type))
{
797
    return user_access('translate content');
800
  }
810
  return FALSE;
820
}
83
84
/**
85
 * Implementation of hook_perm().
86
 */
8737
function translation_perm() {
88
  return array(
891
    'translate content' => t('Translate website content.'),
901
  );
910
}
92
93
/**
94
 * Implementation of hook_form_alter().
95
 *
96
 * - Add translation option to content type form.
97
 * - Alters language fields on node forms when a translation
98
 *   is about to be created.
99
 */
10037
function translation_form_alter(&$form, $form_state, $form_id) {
10122
  if ($form_id == 'node_type_form') {
102
    // Add translation option to content type form.
1033
   
$form['workflow']['language_content_type']['#options'][TRANSLATION_ENABLED]
= t('Enabled, with translation');
104
    // Description based on text from locale.module.
1053
    $form['workflow']['language_content_type']['#description'] = t('Enable
multilingual support for this content type. If enabled, a language
selection field will be added to the editing form, allowing you to select
from one of the <a href="!languages">enabled languages</a>. You can also
turn on translation for this content type, which lets you have content
translated to any of the enabled languages. If disabled, new posts are
saved with the default language. Existing content will not be affected by
changing this option.', array('!languages' =>
url('admin/settings/language')));
1063
  }
10719
  elseif (isset($form['#id']) && $form['#id'] == 'node-form' &&
translation_supported_type($form['#node']->type)) {
1084
    $node = $form['#node'];
1094
    if (!empty($node->translation_source)) {
110
      // We are creating a translation. Add values and lock language
field.
1111
      $form['translation_source'] = array('#type' => 'value', '#value' =>
$node->translation_source);
1121
      $form['language']['#disabled'] = TRUE;
1131
    }
1143
    elseif (!empty($node->nid) && !empty($node->tnid)) {
115
      // Disable languages for existing translations, so it is not possible
to switch this
116
      // node to some language which is already in the translation set.
Also remove the
117
      // language neutral option.
1182
      unset($form['language']['#options']['']);
1192
      foreach (translation_node_get_translations($node->tnid) as
$translation) {
1202
        if ($translation->nid != $node->nid) {
1212
          unset($form['language']['#options'][$translation->language]);
1222
        }
1232
      }
124
      // Add translation values and workflow options.
1252
      $form['tnid'] = array('#type' => 'value', '#value' => $node->tnid);
1262
      $form['translation'] = array(
1272
        '#type' => 'fieldset',
1282
        '#title' => t('Translation settings'),
1292
        '#access' => user_access('translate content'),
1302
        '#collapsible' => TRUE,
1312
        '#collapsed' => !$node->translate,
1322
        '#tree' => TRUE,
1332
        '#weight' => 30,
134
      );
1352
      if ($node->tnid == $node->nid) {
136
        // This is the source node of the translation
1371
        $form['translation']['retranslate'] = array(
1381
          '#type' => 'checkbox',
1391
          '#title' => t('Flag translations as outdated'),
1401
          '#default_value' => 0,
1411
          '#description' => t('If you made a significant change, which
means translations should be updated, you can flag all translations of this
post as outdated. This will not change any other property of those posts,
like whether they are published or not.'),
142
        );
1431
        $form['translation']['status'] = array('#type' => 'value', '#value'
=> 0);
1441
      }
145
      else {
1461
        $form['translation']['status'] = array(
1471
          '#type' => 'checkbox',
1481
          '#title' => t('This translation needs to be updated'),
1491
          '#default_value' => $node->translate,
1501
          '#description' => t('When this option is checked, this
translation needs to be updated because the source post has changed.
Uncheck when the translation is up to date again.'),
151
        );
152
      }
1532
    }
1544
  }
15522
}
156
157
/**
158
 * Implementation of hook_link().
159
 *
160
 * Display translation links with native language names, if this node
161
 * is part of a translation set.
162
 */
16337
function translation_link($type, $node = NULL, $teaser = FALSE) {
1644
  $links = array();
1654
  if ($type == 'node' && ($node->tnid) && $translations =
translation_node_get_translations($node->tnid)) {
166
    // Do not show link to the same node.
1673
    unset($translations[$node->language]);
1683
    $languages = language_list();
1693
    foreach ($translations as $language => $translation) {
1703
      $links["node_translation_$language"] = array(
1713
        'title' => $languages[$language]->native,
1723
        'href' => "node/$translation->nid",
1733
        'language' => $languages[$language],
1743
        'attributes' => array('title' => $translation->title, 'class' =>
'translation-link')
1753
      );
1763
    }
1773
  }
1784
  return $links;
1790
}
180
181
/**
182
 * Implementation of hook_nodeapi().
183
 *
184
 * Manages translation information for nodes.
185
 */
18637
function translation_nodeapi(&$node, $op, $teaser, $page) {
187
  // Only act if we are dealing with a content type supporting
translations.
18814
  if (!translation_supported_type($node->type)) {
1890
    return;
1900
  }
191
192
  switch ($op) {
19314
    case 'prepare':
1944
      if (empty($node->nid) && isset($_GET['translation']) &&
isset($_GET['language']) &&
1951
          ($source_nid = $_GET['translation']) && ($language =
$_GET['language']) &&
1964
          (user_access('translate content'))) {
197
        // We are translating a node from a source node, so
198
        // load the node to be translated and populate fields.
1991
        $node->language = $language;
2001
        $node->translation_source = node_load($source_nid);
2011
        $node->title = $node->translation_source->title;
2021
        $node->body = $node->translation_source->body;
203
        // Let every module add custom translated fields.
2041
        node_invoke_nodeapi($node, 'prepare translation');
2051
      }
2064
      break;
207
20813
    case 'insert':
2092
      if (!empty($node->translation_source)) {
2101
        if ($node->translation_source->tnid) {
211
          // Add node to existing translation set.
2120
          $tnid = $node->translation_source->tnid;
2130
        }
214
        else {
215
          // Create new translation set, using nid from the source node.
2161
          $tnid = $node->translation_source->nid;
2171
          db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid =
%d", $tnid, 0, $node->translation_source->nid);
218
        }
2191
        db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid =
%d", $tnid, 0, $node->nid);
2201
      }
2212
      break;
222
22313
    case 'update':
2242
      if (isset($node->translation) && $node->translation &&
!empty($node->language) && $node->tnid) {
225
        // Update translation information.
2262
        db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid =
%d", $node->tnid, $node->translation['status'], $node->nid);
2272
        if (!empty($node->translation['retranslate'])) {
228
          // This is the source node, asking to mark all translations
outdated.
2291
          db_query("UPDATE {node} SET translate = 1 WHERE tnid = %d AND nid
<> %d", $node->tnid, $node->nid);
2301
        }
2312
      }
2322
      break;
233
23413
    case 'delete':
2350
      translation_remove_from_set($node);
2360
      break;
2370
  }
23814
}
239
240
/**
241
 * Remove a node from its translation set (if any)
242
 * and update the set accordingly.
243
 */
24437
function translation_remove_from_set($node) {
2450
  if (isset($node->tnid)) {
2460
    if (db_result(db_query('SELECT COUNT(*) FROM {node} WHERE tnid = %d',
$node->tnid)) <= 2) {
247
      // There would only be one node left in the set: remove the set
altogether.
2480
      db_query('UPDATE {node} SET tnid = 0, translate = 0 WHERE tnid = %d',
$node->tnid);
2490
    }
250
    else {
2510
      db_query('UPDATE {node} SET tnid = 0, translate = 0 WHERE nid = %d',
$node->nid);
252
253
      // If the node being removed was the source of the translation set,
254
      // we pick a new source - preferably one that is up to date.
2550
      if ($node->tnid == $node->nid) {
2560
        $new_tnid = db_result(db_query('SELECT nid FROM {node} WHERE tnid =
%d ORDER BY translate ASC, nid ASC', $node->tnid));
2570
        db_query('UPDATE {node} SET tnid = %d WHERE tnid = %d', $new_tnid,
$node->tnid);
2580
      }
259
    }
2600
  }
2610
}
262
263
/**
264
 * Get all nodes in a translation set, represented by $tnid.
265
 *
266
 * @param $tnid
267
 *   The translation source nid of the translation set, the identifier
268
 *   of the node used to derive all translations in the set.
269
 * @return
270
 *   Array of partial node objects (nid, title, language) representing
271
 *   all nodes in the translation set, in effect all translations
272
 *   of node $tnid, including node $tnid itself. Because these are
273
 *   partial nodes, you need to node_load() the full node, if you
274
 *   need more properties. The array is indexed by language code.
275
 */
27637
function translation_node_get_translations($tnid) {
2776
  static $translations = array();
278
2796
  if (is_numeric($tnid) && $tnid) {
2806
    if (!isset($translations[$tnid])) {
2816
      $translations[$tnid] = array();
2826
      $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.language
FROM {node} n WHERE n.tnid = %d'), $tnid);
2836
      while ($node = db_fetch_object($result)) {
2846
        $translations[$tnid][$node->language] = $node;
2856
      }
2866
    }
2876
    return $translations[$tnid];
2880
  }
2890
}
290
291
/**
292
 * Returns whether the given content type has support for translations.
293
 *
294
 * @return
295
 *   Boolean value.
296
 */
29737
function translation_supported_type($type) {
29814
  return variable_get('language_content_type_' . $type, 0) ==
TRANSLATION_ENABLED;
2990
}
300
301
/**
302
 * Return paths of all translations of a node, based on
303
 * its Drupal path.
304
 *
305
 * @param $path
306
 *   A Drupal path, for example node/432.
307
 * @return
308
 *   An array of paths of translations of the node accessible
309
 *   to the current user keyed with language codes.
310
 */
31137
function translation_path_get_translations($path) {
3120
  $paths = array();
313
  // Check for a node related path, and for its translations.
3140
  if ((preg_match("!^node/([0-9]+)(/.+|)$!", $path, $matches)) && ($node =
node_load((int)$matches[1])) && !empty($node->tnid)) {
3150
    foreach (translation_node_get_translations($node->tnid) as $language =>
$translation_node) {
3160
      $paths[$language] = 'node/' . $translation_node->nid . $matches[2];
3170
    }
3180
  }
3190
  return $paths;
3200
}
321
322
/**
323
 * Implementation of hook_alter_translation_link().
324
 *
325
 * Replaces links with pointers to translated versions of the content.
326
 */
32737
function translation_translation_link_alter(&$links, $path) {
3280
  if ($paths = translation_path_get_translations($path)) {
3290
    foreach ($links as $langcode => $link) {
3300
      if (isset($paths[$langcode])) {
331
        // Translation in a different node.
3320
        $links[$langcode]['href'] = $paths[$langcode];
3330
      }
334
      else {
335
        // No translation in this language, or no permission to view.
3360
        unset($links[$langcode]);
337
      }
3380
    }
3390
  }
3400
}
341
34237