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

Line #Times calledCode
1
<?php
2
// $Id: translation.module,v 1.33 2008/10/09 18:32:47 webchick 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
 */
2654
define('TRANSLATION_ENABLED', 2);
27
28
/**
29
 * Implementation of hook_help().
30
 */
3154
function translation_help($path, $arg) {
32
  switch ($path) {
3337
    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;
4737
    case 'node/%/translate':
482
      $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>';
492
      return $output;
500
  }
5135
}
52
53
/**
54
 * Implementation of hook_menu().
55
 */
5654
function translation_menu() {
574
  $items = array();
584
  $items['node/%node/translate'] = array(
594
    'title' => 'Translate',
604
    'page callback' => 'translation_node_overview',
614
    'page arguments' => array(1),
624
    'access callback' => '_translation_tab_access',
634
    'access arguments' => array(1),
644
    'type' => MENU_LOCAL_TASK,
654
    'weight' => 2,
66
  );
674
  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
 */
7754
function _translation_tab_access($node) {
7812
  if (!empty($node->language) && translation_supported_type($node->type)) {
7912
    return user_access('translate content');
800
  }
810
  return FALSE;
820
}
83
84
/**
85
 * Implementation of hook_perm().
86
 */
8754
function translation_perm() {
88
  return array(
89
    'translate content' => array(
902
      'title' => t('Tranlate content'),
912
      'description' => t('Translate website content.'),
922
    ),
932
  );
940
}
95
96
/**
97
 * Implementation of hook_form_alter().
98
 *
99
 * - Add translation option to content type form.
100
 * - Alters language fields on node forms when a translation
101
 *   is about to be created.
102
 */
10354
function translation_form_alter(&$form, $form_state, $form_id) {
10430
  if ($form_id == 'node_type_form') {
105
    // Add translation option to content type form.
1063
   
$form['workflow']['language_content_type']['#options'][TRANSLATION_ENABLED]
= t('Enabled, with translation');
107
    // Description based on text from locale.module.
1083
    $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')));
1093
  }
11027
  elseif (isset($form['#id']) && $form['#id'] == 'node-form' &&
translation_supported_type($form['#node']->type)) {
1116
    $node = $form['#node'];
1126
    if (!empty($node->translation_source)) {
113
      // We are creating a translation. Add values and lock language field.
1142
      $form['translation_source'] = array('#type' => 'value', '#value' =>
$node->translation_source);
1152
      $form['language']['#disabled'] = TRUE;
1162
    }
1174
    elseif (!empty($node->nid) && !empty($node->tnid)) {
118
      // Disable languages for existing translations, so it is not possible
to switch this
119
      // node to some language which is already in the translation set.
Also remove the
120
      // language neutral option.
1212
      unset($form['language']['#options']['']);
1222
      foreach (translation_node_get_translations($node->tnid) as
$translation) {
1232
        if ($translation->nid != $node->nid) {
1242
          unset($form['language']['#options'][$translation->language]);
1252
        }
1262
      }
127
      // Add translation values and workflow options.
1282
      $form['tnid'] = array('#type' => 'value', '#value' => $node->tnid);
1292
      $form['translation'] = array(
1302
        '#type' => 'fieldset',
1312
        '#title' => t('Translation settings'),
1322
        '#access' => user_access('translate content'),
1332
        '#collapsible' => TRUE,
1342
        '#collapsed' => !$node->translate,
1352
        '#tree' => TRUE,
1362
        '#weight' => 30,
137
      );
1382
      if ($node->tnid == $node->nid) {
139
        // This is the source node of the translation
1401
        $form['translation']['retranslate'] = array(
1411
          '#type' => 'checkbox',
1421
          '#title' => t('Flag translations as outdated'),
1431
          '#default_value' => 0,
1441
          '#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.'),
145
        );
1461
        $form['translation']['status'] = array('#type' => 'value', '#value'
=> 0);
1471
      }
148
      else {
1491
        $form['translation']['status'] = array(
1501
          '#type' => 'checkbox',
1511
          '#title' => t('This translation needs to be updated'),
1521
          '#default_value' => $node->translate,
1531
          '#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.'),
154
        );
155
      }
1562
    }
1576
  }
15830
}
159
160
/**
161
 * Implementation of hook_link().
162
 *
163
 * Display translation links with native language names, if this node
164
 * is part of a translation set.
165
 */
16654
function translation_link($type, $node = NULL, $teaser = FALSE) {
1678
  $links = array();
1688
  if ($type == 'node' && ($node->tnid) && $translations =
translation_node_get_translations($node->tnid)) {
169
    // Do not show link to the same node.
1705
    unset($translations[$node->language]);
1715
    $languages = language_list();
1725
    foreach ($languages as $langcode => $language) {
1735
      if (isset($translations[$langcode])) {
1745
        $links["node_translation_$langcode"] = array(
1755
          'title' => $language->native,
1765
          'href' => 'node/' . $translations[$langcode]->nid,
1775
          'language' => $language,
1785
          'attributes' => array('title' => $translations[$langcode]->title,
'class' => 'translation-link')
1795
        );
1805
      }
1815
    }
1825
  }
1838
  return $links;
1840
}
185
186
/**
187
 * Implementation of hook_nodeapi_prepare().
188
 */
18954
function translation_nodeapi_prepare(&$node, $teaser, $page) {
190
  // Only act if we are dealing with a content type supporting
translations.
1916
  if (translation_supported_type($node->type)) {
1926
    if (empty($node->nid) && isset($_GET['translation']) &&
isset($_GET['language']) &&
1932
        ($source_nid = $_GET['translation']) && ($language =
$_GET['language']) &&
1946
        (user_access('translate content'))) {
195
      // We are translating a node from a source node, so
196
      // load the node to be translated and populate fields.
1972
      $node->language = $language;
1982
      $node->translation_source = node_load($source_nid);
1992
      $node->title = $node->translation_source->title;
2002
      $node->body = $node->translation_source->body;
201
      // Let every module add custom translated fields.
2022
      node_invoke_nodeapi($node, 'prepare_translation');
2032
    }
2046
  }
2056
}
206
207
/**
208
 * Implementation of hook_nodeapi_insert().
209
 */
21054
function translation_nodeapi_insert(&$node, $teaser, $page) {
211
  // Only act if we are dealing with a content type supporting
translations.
2124
  if (translation_supported_type($node->type)) {
2134
    if (!empty($node->translation_source)) {
2142
      if ($node->translation_source->tnid) {
215
        // Add node to existing translation set.
2160
        $tnid = $node->translation_source->tnid;
2170
      }
218
      else {
219
        // Create new translation set, using nid from the source node.
2202
        $tnid = $node->translation_source->nid;
2212
        db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid =
%d", $tnid, 0, $node->translation_source->nid);
222
      }
2232
      db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid =
%d", $tnid, 0, $node->nid);
2242
    }
2254
  }
2264
}
227
228
/**
229
 * Implementation of hook_nodeapi_update().
230
 */
23154
function translation_nodeapi_update(&$node, $teaser, $page) {
232
  // Only act if we are dealing with a content type supporting
translations.
2332
  if (translation_supported_type($node->type)) {
2342
    if (isset($node->translation) && $node->translation &&
!empty($node->language) && $node->tnid) {
235
      // Update translation information.
2362
      db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid =
%d", $node->tnid, $node->translation['status'], $node->nid);
2372
      if (!empty($node->translation['retranslate'])) {
238
        // This is the source node, asking to mark all translations
outdated.
2391
        db_query("UPDATE {node} SET translate = 1 WHERE tnid = %d AND nid
<> %d", $node->tnid, $node->nid);
2401
      }
2412
    }
2422
  }
2432
}
244
245
/**
246
 * Implementation of hook_nodeapi_delete().
247
 */
24854
function translation_nodeapi_delete(&$node, $teaser, $page) {
249
  // Only act if we are dealing with a content type supporting
translations.
2500
  if (translation_supported_type($node->type)) {
2510
    translation_remove_from_set($node);
2520
  }
2530
}
254
 
255
/**
256
 * Remove a node from its translation set (if any)
257
 * and update the set accordingly.
258
 */
25954
function translation_remove_from_set($node) {
2600
  if (isset($node->tnid)) {
2610
    if (db_result(db_query('SELECT COUNT(*) FROM {node} WHERE tnid = %d',
$node->tnid)) == 1) {
262
      // There is only one node left in the set: remove the set altogether.
2630
      db_query('UPDATE {node} SET tnid = 0, translate = 0 WHERE tnid = %d',
$node->tnid);
2640
    }
265
    else {
2660
      db_query('UPDATE {node} SET tnid = 0, translate = 0 WHERE nid = %d',
$node->nid);
267
268
      // If the node being removed was the source of the translation set,
269
      // we pick a new source - preferably one that is up to date.
2700
      if ($node->tnid == $node->nid) {
2710
        $new_tnid = db_result(db_query('SELECT nid FROM {node} WHERE tnid =
%d ORDER BY translate ASC, nid ASC', $node->tnid));
2720
        db_query('UPDATE {node} SET tnid = %d WHERE tnid = %d', $new_tnid,
$node->tnid);
2730
      }
274
    }
2750
  }
2760
}
277
278
/**
279
 * Get all nodes in a translation set, represented by $tnid.
280
 *
281
 * @param $tnid
282
 *   The translation source nid of the translation set, the identifier
283
 *   of the node used to derive all translations in the set.
284
 * @return
285
 *   Array of partial node objects (nid, title, language) representing
286
 *   all nodes in the translation set, in effect all translations
287
 *   of node $tnid, including node $tnid itself. Because these are
288
 *   partial nodes, you need to node_load() the full node, if you
289
 *   need more properties. The array is indexed by language code.
290
 */
29154
function translation_node_get_translations($tnid) {
2928
  static $translations = array();
293
2948
  if (is_numeric($tnid) && $tnid) {
2958
    if (!isset($translations[$tnid])) {
2968
      $translations[$tnid] = array();
2978
      $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.language
FROM {node} n WHERE n.tnid = %d'), $tnid);
2988
      while ($node = db_fetch_object($result)) {
2998
        $translations[$tnid][$node->language] = $node;
3008
      }
3018
    }
3028
    return $translations[$tnid];
3030
  }
3040
}
305
306
/**
307
 * Returns whether the given content type has support for translations.
308
 *
309
 * @return
310
 *   Boolean value.
311
 */
31254
function translation_supported_type($type) {
31322
  return variable_get('language_content_type_' . $type, 0) ==
TRANSLATION_ENABLED;
3140
}
315
316
/**
317
 * Return paths of all translations of a node, based on
318
 * its Drupal path.
319
 *
320
 * @param $path
321
 *   A Drupal path, for example node/432.
322
 * @return
323
 *   An array of paths of translations of the node accessible
324
 *   to the current user keyed with language codes.
325
 */
32654
function translation_path_get_translations($path) {
3270
  $paths = array();
328
  // Check for a node related path, and for its translations.
3290
  if ((preg_match("!^node/([0-9]+)(/.+|)$!", $path, $matches)) && ($node =
node_load((int)$matches[1])) && !empty($node->tnid)) {
3300
    foreach (translation_node_get_translations($node->tnid) as $language =>
$translation_node) {
3310
      $paths[$language] = 'node/' . $translation_node->nid . $matches[2];
3320
    }
3330
  }
3340
  return $paths;
3350
}
336
337
/**
338
 * Implementation of hook_alter_translation_link().
339
 *
340
 * Replaces links with pointers to translated versions of the content.
341
 */
34254
function translation_translation_link_alter(&$links, $path) {
3430
  if ($paths = translation_path_get_translations($path)) {
3440
    foreach ($links as $langcode => $link) {
3450
      if (isset($paths[$langcode])) {
346
        // Translation in a different node.
3470
        $links[$langcode]['href'] = $paths[$langcode];
3480
      }
349
      else {
350
        // No translation in this language, or no permission to view.
3510
        unset($links[$langcode]);
352
      }
3530
    }
3540
  }
3550
}
356
35754