Merge branch '8.x' into config-next-1626584-sun
[sandbox/heyrocker/1145636.git] / core / modules / translation / translation.module
1 <?php
2
3 /**
4 * @file
5 * Manages content translations.
6 *
7 * Translations are managed in sets of posts, which represent the same
8 * information in different languages. Only content types for which the
9 * administrator has explicitly enabled translations could have translations
10 * associated. Translations are managed in sets with exactly one source post
11 * per set. The source post is used to translate to different languages, so if
12 * the source post is significantly updated, the editor can decide to mark all
13 * translations outdated.
14 *
15 * The node table stores the values used by this module:
16 * - tnid: Integer for the translation set ID, which equals the node ID of the
17 * source post.
18 * - translate: Integer flag, either indicating that the translation is up to
19 * date (0) or needs to be updated (1).
20 */
21
22 use Drupal\node\Node;
23
24 /**
25 * Implements hook_help().
26 */
27 function translation_help($path, $arg) {
28 switch ($path) {
29 case 'admin/help#translation':
30 $output = '';
31 $output .= '<h3>' . t('About') . '</h3>';
32 $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. For more information, see the online handbook entry for <a href="@translation">Content Translation module</a>.', array('@locale' => url('admin/help/locale'), '@translation' => 'http://drupal.org/documentation/modules/translation/')) . '</p>';
33 $output .= '<h3>' . t('Uses') . '</h3>';
34 $output .= '<dl>';
35 $output .= '<dt>' . t('Configuring content types for translation') . '</dt>';
36 $output .= '<dd>' . t('To configure a particular content type for translation, visit the <a href="@content-types">Content types</a> page, and click the <em>edit</em> link for the content type. In the <em>Publishing options</em> section, select <em>Enabled, with translation</em> under <em>Multilingual support</em>.', array('@content-types' => url('admin/structure/types'))) . '</dd>';
37 $output .= '<dt>' . t('Assigning a language to content') . '</dt>';
38 $output .= '<dd>' . t('Use the <em>Language</em> drop down to select the appropriate language when creating or editing content.') . '</dd>';
39 $output .= '<dt>' . t('Translating content') . '</dt>';
40 $output .= '<dd>' . t('Users with the <em>translate content</em> permission can translate content, if the content type has been configured to allow translations. To translate content, select the <em>Translations</em> tab when viewing the content, select the language for which you wish to provide content, and then enter the content.') . '</dd>';
41 $output .= '<dt>' . t('Maintaining translations') . '</dt>';
42 $output .= '<dd>' . t('If editing content in one language requires that translated versions also be updated to reflect the change, use the <em>Flag translations as outdated</em> check box to mark the translations as outdated and in need of revision. Individual translations may also be marked for revision by selecting the <em>This translation needs to be updated</em> check box on the translation editing form.') . '</dd>';
43 $output .= '</dl>';
44 return $output;
45 case 'node/%/translate':
46 $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/config/regional/language'))) . '</p>';
47 return $output;
48 }
49 }
50
51 /**
52 * Implements hook_menu().
53 */
54 function translation_menu() {
55 $items = array();
56 $items['node/%node/translate'] = array(
57 'title' => 'Translations',
58 'page callback' => 'translation_node_overview',
59 'page arguments' => array(1),
60 'access callback' => '_translation_tab_access',
61 'access arguments' => array(1),
62 'type' => MENU_LOCAL_TASK,
63 'weight' => 2,
64 'file' => 'translation.pages.inc',
65 );
66 return $items;
67 }
68
69 /**
70 * Access callback: Checks that the user has permission to 'translate content'.
71 *
72 * Only displays the translation tab for nodes that are not language-neutral
73 * of types that have translation enabled.
74 *
75 * @param $node
76 * A node entity.
77 *
78 * @return
79 * TRUE if the translation tab should be displayed, FALSE otherwise.
80 *
81 * @see translation_menu()
82 */
83 function _translation_tab_access($node) {
84 if ($node->langcode != LANGUAGE_NOT_SPECIFIED && translation_supported_type($node->type) && node_access('view', $node)) {
85 return user_access('translate content');
86 }
87 return FALSE;
88 }
89
90 /**
91 * Implements hook_admin_paths().
92 */
93 function translation_admin_paths() {
94 if (variable_get('node_admin_theme')) {
95 $paths = array(
96 'node/*/translate' => TRUE,
97 );
98 return $paths;
99 }
100 }
101
102 /**
103 * Implements hook_permission().
104 */
105 function translation_permission() {
106 return array(
107 'translate content' => array(
108 'title' => t('Translate content'),
109 ),
110 );
111 }
112
113 /**
114 * Implements hook_form_FORM_ID_alter() for node_type_form().
115 */
116 function translation_form_node_type_form_alter(&$form, &$form_state) {
117 // Hide form element if default language is a constant.
118 // TODO: When form #states allows OR values change this to use form #states.
119 $form['#attached']['js'] = array(
120 drupal_get_path('module', 'translation') . '/translation.js',
121 );
122 // Add translation option to content type form.
123 $form['language']['node_type_language_translation_enabled'] = array(
124 '#type' => 'checkbox',
125 '#title' => t('Enable translation'),
126 '#default_value' => variable_get('node_type_language_translation_enabled_' . $form['#node_type']->type, FALSE),
127 '#element_validate' => array('translation_node_type_language_translation_enabled_validate'),
128 '#prefix' => "<label class='form-item-node-type-language-translation-enabled'>" . t('Translation') . "</label>",
129 );
130 }
131
132 /**
133 * Checks if translation can be enabled.
134 *
135 * If language is set to one of the special languages
136 * and language selector is not hidden, translation cannot be enabled.
137 */
138 function translation_node_type_language_translation_enabled_validate($element, &$form_state, $form) {
139 if (language_is_locked($form_state['values']['node_type_language_default']) && $form_state['values']['node_type_language_hidden'] && $form_state['values']['node_type_language_translation_enabled']) {
140 foreach (language_list(LANGUAGE_LOCKED) as $language) {
141 $locked_languages[] = $language->name;
142 }
143 form_set_error('node_type_language_translation_enabled', t('Translation is not supported if language is always one of: @locked_languages', array('@locked_languages' => implode(", ", $locked_languages))));
144 }
145 }
146
147 /**
148 * Implements hook_form_BASE_FORM_ID_alter() for node_form().
149 *
150 * Alters language fields on node edit forms when a translation is about to be
151 * created.
152 *
153 * @see node_form()
154 */
155 function translation_form_node_form_alter(&$form, &$form_state) {
156 if (translation_supported_type($form['#node']->type)) {
157 $node = $form['#node'];
158
159 if (!empty($node->translation_source)) {
160 // We are creating a translation. Add values and lock language field.
161 $form['translation_source'] = array('#type' => 'value', '#value' => $node->translation_source);
162 $form['langcode']['#disabled'] = TRUE;
163 }
164 elseif (!empty($node->nid) && !empty($node->tnid)) {
165 // Disable languages for existing translations, so it is not possible
166 // to switch this node to some language which is already in the
167 // translation set. Also remove the language neutral option.
168 unset($form['langcode']['#options'][LANGUAGE_NOT_SPECIFIED]);
169 foreach (translation_node_get_translations($node->tnid) as $langcode => $translation) {
170 if ($translation->nid != $node->nid) {
171 unset($form['langcode']['#options'][$langcode]);
172 }
173 }
174 // Add translation values and workflow options.
175 $form['tnid'] = array('#type' => 'value', '#value' => $node->tnid);
176 $form['translation'] = array(
177 '#type' => 'fieldset',
178 '#title' => t('Translation settings'),
179 '#access' => user_access('translate content'),
180 '#collapsible' => TRUE,
181 '#collapsed' => !$node->translate,
182 '#tree' => TRUE,
183 '#weight' => 30,
184 );
185 if ($node->tnid == $node->nid) {
186 // This is the source node of the translation.
187 $form['translation']['retranslate'] = array(
188 '#type' => 'checkbox',
189 '#title' => t('Flag translations as outdated'),
190 '#default_value' => 0,
191 '#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.'),
192 );
193 $form['translation']['status'] = array('#type' => 'value', '#value' => 0);
194 }
195 else {
196 $form['translation']['status'] = array(
197 '#type' => 'checkbox',
198 '#title' => t('This translation needs to be updated'),
199 '#default_value' => $node->translate,
200 '#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.'),
201 );
202 }
203 }
204 }
205 }
206
207 /**
208 * Implements hook_node_view().
209 *
210 * Displays translation links with language names if this node is part of a
211 * translation set. If no language provider is enabled, "fall back" to simple
212 * links built through the result of translation_node_get_translations().
213 */
214 function translation_node_view(Node $node, $view_mode) {
215 // If the site has no translations or is not multilingual we have no content
216 // translation links to display.
217 if (isset($node->tnid) && language_multilingual() && $translations = translation_node_get_translations($node->tnid)) {
218 $languages = language_list(LANGUAGE_ALL);
219
220 // There might be a language provider enabled defining custom language
221 // switch links which need to be taken into account while generating the
222 // content translation links. As custom language switch links are available
223 // only for configurable language types and interface language is the only
224 // configurable language type in core, we use it as default. Contributed
225 // modules can change this behavior by setting the system variable below.
226 $type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE);
227 $custom_links = language_negotiation_get_switch_links($type, "node/$node->nid");
228 $links = array();
229
230 foreach ($translations as $langcode => $translation) {
231 // Do not show links to the same node or to unpublished translations.
232 if ($translation->status && isset($languages[$langcode]) && $langcode != $node->langcode) {
233 $key = "translation_$langcode";
234
235 if (isset($custom_links->links[$langcode])) {
236 $links[$key] = $custom_links->links[$langcode];
237 }
238 else {
239 $links[$key] = array(
240 'href' => "node/{$translation->nid}",
241 'title' => language_name($langcode),
242 'language' => $languages[$langcode],
243 );
244 }
245
246 // Custom switch links are more generic than content translation links,
247 // hence we override existing attributes with the ones below.
248 $links[$key] += array('attributes' => array());
249 $attributes = array(
250 'title' => $translation->title,
251 'class' => array('translation-link'),
252 );
253 $links[$key]['attributes'] = $attributes + $links[$key]['attributes'];
254 }
255 }
256
257 $node->content['links']['translation'] = array(
258 '#theme' => 'links__node__translation',
259 '#links' => $links,
260 '#attributes' => array('class' => array('links', 'inline')),
261 );
262 }
263 }
264
265 /**
266 * Implements hook_node_prepare().
267 */
268 function translation_node_prepare(Node $node) {
269 // Only act if we are dealing with a content type supporting translations.
270 if (translation_supported_type($node->type) &&
271 // And it's a new node.
272 empty($node->nid) &&
273 // And the user has permission to translate content.
274 user_access('translate content') &&
275 // And the $_GET variables are set properly.
276 isset($_GET['translation']) &&
277 isset($_GET['target']) &&
278 is_numeric($_GET['translation'])) {
279
280 $source_node = node_load($_GET['translation']);
281 if (empty($source_node) || !node_access('view', $source_node)) {
282 // Source node not found or no access to view. We should not check
283 // for edit access, since the translator might not have permissions
284 // to edit the source node but should still be able to translate.
285 return;
286 }
287
288 $language_list = language_list();
289 $langcode = $_GET['target'];
290 if (!isset($language_list[$langcode]) || ($source_node->langcode == $langcode)) {
291 // If not supported language, or same language as source node, break.
292 return;
293 }
294
295 // Ensure we don't have an existing translation in this language.
296 if (!empty($source_node->tnid)) {
297 $translations = translation_node_get_translations($source_node->tnid);
298 if (isset($translations[$langcode])) {
299 drupal_set_message(t('A translation of %title in %language already exists, a new %type will be created instead of a translation.', array('%title' => $source_node->title, '%language' => $language_list[$langcode]->name, '%type' => $node->type)), 'error');
300 return;
301 }
302 }
303
304 // Populate fields based on source node.
305 $node->langcode = $langcode;
306 $node->translation_source = $source_node;
307 $node->title = $source_node->title;
308
309 // Add field translations and let other modules module add custom translated
310 // fields.
311 field_attach_prepare_translation('node', $node, $node->langcode, $source_node, $source_node->langcode);
312 }
313 }
314
315 /**
316 * Implements hook_node_insert().
317 */
318 function translation_node_insert(Node $node) {
319 // Only act if we are dealing with a content type supporting translations.
320 if (translation_supported_type($node->type)) {
321 if (!empty($node->translation_source)) {
322 if ($node->translation_source->tnid) {
323 // Add node to existing translation set.
324 $tnid = $node->translation_source->tnid;
325 }
326 else {
327 // Create new translation set, using nid from the source node.
328 $tnid = $node->translation_source->nid;
329 db_update('node')
330 ->fields(array(
331 'tnid' => $tnid,
332 'translate' => 0,
333 ))
334 ->condition('nid', $node->translation_source->nid)
335 ->execute();
336 }
337 db_update('node')
338 ->fields(array(
339 'tnid' => $tnid,
340 'translate' => 0,
341 ))
342 ->condition('nid', $node->nid)
343 ->execute();
344 // Save tnid to avoid loss in case of resave.
345 $node->tnid = $tnid;
346 }
347 }
348 }
349
350 /**
351 * Implements hook_node_update().
352 */
353 function translation_node_update(Node $node) {
354 // Only act if we are dealing with a content type supporting translations.
355 if (translation_supported_type($node->type)) {
356 if (isset($node->translation) && $node->translation && !empty($node->langcode) && $node->tnid) {
357 // Update translation information.
358 db_update('node')
359 ->fields(array(
360 'tnid' => $node->tnid,
361 'translate' => $node->translation['status'],
362 ))
363 ->condition('nid', $node->nid)
364 ->execute();
365 if (!empty($node->translation['retranslate'])) {
366 // This is the source node, asking to mark all translations outdated.
367 db_update('node')
368 ->fields(array('translate' => 1))
369 ->condition('nid', $node->nid, '<>')
370 ->condition('tnid', $node->tnid)
371 ->execute();
372 }
373 }
374 }
375 }
376
377 /**
378 * Implements hook_node_validate().
379 *
380 * Ensures that duplicate translations can't be created for the same source.
381 */
382 function translation_node_validate(Node $node, $form) {
383 // Only act on translatable nodes with a tnid or translation_source.
384 if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form['#node']->translation_source->nid))) {
385 $tnid = !empty($node->tnid) ? $node->tnid : $form['#node']->translation_source->nid;
386 $translations = translation_node_get_translations($tnid);
387 if (isset($translations[$node->langcode]) && $translations[$node->langcode]->nid != $node->nid) {
388 form_set_error('langcode', t('There is already a translation in this language.'));
389 }
390 }
391 }
392
393 /**
394 * Implements hook_node_predelete().
395 */
396 function translation_node_predelete(Node $node) {
397 // Only act if we are dealing with a content type supporting translations.
398 if (translation_supported_type($node->type)) {
399 translation_remove_from_set($node);
400 }
401 }
402
403 /**
404 * Removes a node from its translation set and updates accordingly.
405 *
406 * @param $node
407 * A node entity.
408 */
409 function translation_remove_from_set($node) {
410 if (isset($node->tnid)) {
411 $query = db_update('node')
412 ->fields(array(
413 'tnid' => 0,
414 'translate' => 0,
415 ));
416 if (db_query('SELECT COUNT(*) FROM {node} WHERE tnid = :tnid', array(':tnid' => $node->tnid))->fetchField() == 1) {
417 // There is only one node left in the set: remove the set altogether.
418 $query
419 ->condition('tnid', $node->tnid)
420 ->execute();
421 }
422 else {
423 $query
424 ->condition('nid', $node->nid)
425 ->execute();
426
427 // If the node being removed was the source of the translation set,
428 // we pick a new source - preferably one that is up to date.
429 if ($node->tnid == $node->nid) {
430 $new_tnid = db_query('SELECT nid FROM {node} WHERE tnid = :tnid ORDER BY translate ASC, nid ASC', array(':tnid' => $node->tnid))->fetchField();
431 db_update('node')
432 ->fields(array('tnid' => $new_tnid))
433 ->condition('tnid', $node->tnid)
434 ->execute();
435 }
436 }
437 }
438 }
439
440 /**
441 * Gets all nodes in a given translation set.
442 *
443 * @param $tnid
444 * The translation source nid of the translation set, the identifier of the
445 * node used to derive all translations in the set.
446 *
447 * @return
448 * Array of partial node objects (nid, title, langcode) representing all
449 * nodes in the translation set, in effect all translations of node $tnid,
450 * including node $tnid itself. Because these are partial nodes, you need to
451 * node_load() the full node, if you need more properties. The array is
452 * indexed by language code.
453 */
454 function translation_node_get_translations($tnid) {
455 if (is_numeric($tnid) && $tnid) {
456 $translations = &drupal_static(__FUNCTION__, array());
457
458 if (!isset($translations[$tnid])) {
459 $translations[$tnid] = array();
460 $result = db_select('node', 'n')
461 ->fields('n', array('nid', 'type', 'uid', 'status', 'title', 'langcode'))
462 ->condition('n.tnid', $tnid)
463 ->addTag('node_access')
464 ->execute();
465
466 foreach ($result as $node) {
467 $translations[$tnid][$node->langcode] = $node;
468 }
469 }
470 return $translations[$tnid];
471 }
472 }
473
474 /**
475 * Returns whether the given content type has support for translations.
476 *
477 * @return
478 * TRUE if translation is supported, and FALSE if not.
479 */
480 function translation_supported_type($type) {
481 return variable_get('node_type_language_translation_enabled_' . $type, FALSE);
482 }
483
484 /**
485 * Returns the paths of all translations of a node, based on its Drupal path.
486 *
487 * @param $path
488 * A Drupal path, for example node/432.
489 *
490 * @return
491 * An array of paths of translations of the node accessible to the current
492 * user, keyed with language codes.
493 */
494 function translation_path_get_translations($path) {
495 $paths = array();
496 // Check for a node related path, and for its translations.
497 if ((preg_match("!^node/(\d+)(/.+|)$!", $path, $matches)) && ($node = node_load((int) $matches[1])) && !empty($node->tnid)) {
498 foreach (translation_node_get_translations($node->tnid) as $language => $translation_node) {
499 $paths[$language] = 'node/' . $translation_node->nid . $matches[2];
500 }
501 }
502 return $paths;
503 }
504
505 /**
506 * Implements hook_language_switch_links_alter().
507 *
508 * Replaces links with pointers to translated versions of the content.
509 */
510 function translation_language_switch_links_alter(array &$links, $type, $path) {
511 $language_type = variable_get('translation_language_type', LANGUAGE_TYPE_INTERFACE);
512
513 if ($type == $language_type && preg_match("!^node/(\d+)(/.+|)!", $path, $matches)) {
514 $node = node_load((int) $matches[1]);
515
516 if (empty($node->tnid)) {
517 // If the node cannot be found nothing needs to be done. If it does not
518 // have translations it might be a language neutral node, in which case we
519 // must leave the language switch links unaltered. This is true also for
520 // nodes not having translation support enabled.
521 if (empty($node) || $node->langcode == LANGUAGE_NOT_SPECIFIED || !translation_supported_type($node->type)) {
522 return;
523 }
524 $translations = array($node->langcode => $node);
525 }
526 else {
527 $translations = translation_node_get_translations($node->tnid);
528 }
529
530 foreach ($links as $langcode => $link) {
531 if (isset($translations[$langcode]) && $translations[$langcode]->status) {
532 // Translation in a different node.
533 $links[$langcode]['href'] = 'node/' . $translations[$langcode]->nid . $matches[2];
534 }
535 else {
536 // No translation in this language, or no permission to view.
537 unset($links[$langcode]['href']);
538 $links[$langcode]['attributes']['class'][] = 'locale-untranslated';
539 }
540 }
541 }
542 }