Critical bug fix
[project/i18n.git] / i18n.module
1 <?php
2
3 /**
4 * @file
5 * Internationalization (i18n) module.
6 *
7 * This module extends multilingual support being the base module for the i18n package.
8 * - Multilingual variables
9 * - Extended languages for nodes
10 * - Extended language API
11 *
12 * @author Jose A. Reyero, 2004
13 */
14
15 // All multilingual options disabled
16 define('I18N_LANGUAGE_DISABLED', 0);
17 // Language list will include all enabled languages
18 define('I18N_LANGUAGE_ENABLED', 1);
19 // Language list will include also disabled languages
20 define('I18N_LANGUAGE_EXTENDED', 4);
21 // Disabled languages will be hidden when possible
22 define('I18N_LANGUAGE_HIDDEN', 8);
23 // All defined languages will be allowed but hidden when possible
24 define('I18N_LANGUAGE_EXTENDED_NOT_DISPLAYED', I18N_LANGUAGE_EXTENDED | I18N_LANGUAGE_HIDDEN);
25
26 // No multilingual options
27 define('I18N_MODE_NONE', 0);
28 // Localizable object. Run through the localization system
29 define('I18N_MODE_LOCALIZE', 1);
30 // Predefined language for this object and all related ones.
31 define('I18N_MODE_LANGUAGE', 2);
32 // Multilingual objects, translatable but not localizable.
33 define('I18N_MODE_TRANSLATE', 4);
34 // Objects are translatable (if they have language or localizable if not)
35 define('I18N_MODE_MULTIPLE', I18N_MODE_LOCALIZE | I18N_MODE_TRANSLATE);
36
37 /**
38 * Global variable for i18n context language.
39 */
40 define('I18N_LANGUAGE_TYPE_CONTEXT', 'i18n_language_context');
41
42 /**
43 * Implements hook_boot().
44 */
45 function i18n_boot() {
46 // Just make sure the module is loaded for boot and the API is available.
47 }
48
49 /**
50 * Implements hook_hook_info().
51 */
52 function i18n_hook_info() {
53 $hooks['i18n_object_info'] = array(
54 'group' => 'i18n',
55 );
56 return $hooks;
57 }
58
59 /**
60 * WARNING: Obsolete function, use other i18n_language_* instead.
61 *
62 * Get global language object, make sure it is initialized
63 *
64 * @param $language
65 * Language code or language object to convert to valid language object
66 */
67 function i18n_language($language = NULL) {
68 if ($language && ($language_object = i18n_language_object($language))) {
69 return $language_object;
70 }
71 else {
72 return i18n_language_interface();
73 }
74 }
75
76 /**
77 * Get language object from language code or object.
78 *
79 * @param $language
80 * Language code or language object to convert to valid language object.
81 * @return
82 * Language object if this is an object or a valid language code.
83 */
84 function i18n_language_object($language) {
85 if (is_object($language)) {
86 return $language;
87 }
88 else {
89 $list = language_list();
90 return isset($list[$language]) ? $list[$language] : NULL;
91 }
92 }
93
94 /**
95 * Get interface language, make sure it is initialized.
96 */
97 function i18n_language_interface() {
98 if (empty($GLOBALS[LANGUAGE_TYPE_INTERFACE])) {
99 // We don't have language yet, initialize the language system and retry
100 drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE);
101 }
102 return $GLOBALS[LANGUAGE_TYPE_INTERFACE];
103 }
104
105 /**
106 * Get content language, make sure it is initialized.
107 */
108 function i18n_language_content() {
109 if (empty($GLOBALS[LANGUAGE_TYPE_CONTENT])) {
110 // We don't have language yet, initialize the language system and retry
111 drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE);
112 }
113 return $GLOBALS[LANGUAGE_TYPE_CONTENT];
114 }
115
116 /**
117 * Get / set language from current context / content.
118 *
119 * Depending on the page content we may need to use a different language for some operations.
120 *
121 * This should be the language of the specific page content. I.e. node language for node pages
122 * or term language for taxonomy term page.
123 *
124 * @param $language
125 * Optional language object to set for context.
126 * @return
127 * Context language object, which defaults to content language.
128 *
129 * @see hook_i18n_context_language().
130 */
131 function i18n_language_context($language = NULL) {
132 if ($language) {
133 $GLOBALS[I18N_LANGUAGE_TYPE_CONTEXT] = $language;
134 }
135 elseif (!isset($GLOBALS[I18N_LANGUAGE_TYPE_CONTEXT])) {
136 // It will default to content language.
137 $GLOBALS[I18N_LANGUAGE_TYPE_CONTEXT] = i18n_language_content();
138 // Get language from the first module that provides it.
139 foreach (module_implements('i18n_context_language') as $module) {
140 if ($language = module_invoke($module, 'i18n_context_language')) {
141 $GLOBALS[I18N_LANGUAGE_TYPE_CONTEXT] = $language;
142 break;
143 }
144 }
145 }
146 return $GLOBALS[I18N_LANGUAGE_TYPE_CONTEXT];
147 }
148
149 /**
150 * Menu object loader, language
151 */
152 function i18n_language_load($langcode) {
153 $list = language_list();
154 return isset($list[$langcode]) ? $list[$langcode] : FALSE;
155 }
156
157 /**
158 * Get language selector form element
159 */
160 function i18n_element_language_select($default = LANGUAGE_NONE) {
161 if (is_object($default) || is_array($default)) {
162 $default = i18n_object_langcode($default, LANGUAGE_NONE);
163 }
164 return array(
165 '#type' => 'select',
166 '#title' => t('Language'),
167 '#default_value' => $default,
168 '#options' => array(LANGUAGE_NONE => t('Language neutral')) + i18n_language_list(),
169 );
170 }
171
172 /**
173 * Get language field for hook_field_extra_fields()
174 */
175 function i18n_language_field_extra() {
176 return array(
177 'form' => array(
178 'language' => array(
179 'label' => t('Language'),
180 'description' => t('Language selection'),
181 'weight' => 0,
182 ),
183 ),
184 'display' => array(
185 'language' => array(
186 'label' => t('Language'),
187 'description' => t('Language'),
188 'weight' => 0,
189 ),
190 ),
191 );
192 }
193
194 /**
195 * Get full language list
196 *
197 * @todo See about creating a permission for seeing disabled languages
198 */
199 function i18n_language_list($field = 'name', $mode = NULL) {
200 $mode = isset($mode) ? $mode : variable_get('i18n_language_list', I18N_LANGUAGE_ENABLED);
201 return locale_language_list($field, I18N_LANGUAGE_EXTENDED & $mode);
202 }
203
204 /**
205 * Get language name for any defined (enabled or not) language
206 *
207 * @see locale_language_list()
208 */
209 function i18n_language_name($lang) {
210 $list = &drupal_static(__FUNCTION__);
211 if (!isset($list)) {
212 $list = locale_language_list('name', TRUE);
213 }
214 if (!$lang || $lang === LANGUAGE_NONE) {
215 return t('Undefined');
216 }
217 elseif (isset($list[$lang])) {
218 return check_plain($list[$lang]);
219 }
220 else {
221 return t('Unknown');
222 }
223 }
224
225 /**
226 * Get valid language code for current page or check whether the code is a defined language
227 */
228 function i18n_langcode($langcode = NULL) {
229 return $langcode && $langcode !== LANGUAGE_NONE ? $langcode : i18n_language()->language;
230 }
231
232 /**
233 * Implements hook_help().
234 */
235 function i18n_help($path = 'admin/help#i18n', $arg) {
236 switch ($path) {
237 case 'admin/help#i18n' :
238 $output = '<p>' . t('This module improves support for multilingual content in Drupal sites:') . '</p>';
239 $output .= '<ul>';
240 $output .= '<li>' . t('Shows content depending on page language.') . '</li>';
241 $output .= '<li>' . t('Handles multilingual variables.') . '</li>';
242 $output .= '<li>' . t('Extended language option for chosen content types. For these content types transations will be allowed for all defined languages, not only for enabled ones.') . '</li>';
243 $output .= '<li>' . t('Provides a block for language selection and two theme functions: <em>i18n_flags</em> and <em>i18n_links</em>.') . '</li>';
244 $output .= '</ul>';
245 $output .= '<p>' . t('This is the base module for several others adding different features:') . '</p>';
246 $output .= '<ul>';
247 $output .= '<li>' . t('Multilingual menu items.') . '</li>';
248 $output .= '<li>' . t('Multilingual taxonomy adds a language field for taxonomy vocabularies and terms.') . '</li>';
249 $output .= '</ul>';
250 $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@i18n">Internationalization module</a>.', array('@i18n' => 'http://drupal.org/node/133977')) . '</p>';
251 return $output;
252
253 case 'admin/config/language/i18n':
254 $output = '<ul>';
255 $output .= '<li>' . t('To enable multilingual support for specific content types go to <a href="@configure_content_types">configure content types</a>.', array('@configure_content_types' => url('admin/structure/types'))) . '</li>';
256 $output .= '</ul>';
257 return $output;
258 }
259 }
260
261 /**
262 * Implements hook_menu().
263 */
264 function i18n_menu() {
265 $items['admin/config/regional/i18n'] = array(
266 'title' => 'Multilingual settings',
267 'description' => 'Configure extended options for multilingual content and translations.',
268 'page callback' => 'drupal_get_form',
269 'page arguments' => array('variable_module_form', 'i18n'),
270 'access arguments' => array('administer site configuration'),
271 'weight' => 10,
272 );
273 $items['admin/config/regional/i18n/configure'] = array(
274 'title' => 'Multilingual system',
275 'description' => 'Configure extended options for multilingual content and translations.',
276 'type' => MENU_DEFAULT_LOCAL_TASK,
277 );
278 module_load_include('pages.inc', 'i18n');
279 $items += i18n_page_menu_items();
280 return $items;
281 }
282
283 /**
284 * Simple i18n API
285 */
286
287 /**
288 * Switch select Mode on off if enabled
289 *
290 * Usage for disabling content selection for a while then return to previous state
291 *
292 * // Disable selection, but store previous mode
293 * $previous = i18n_select(FALSE);
294 *
295 * // Other code to be run without content selection here
296 * ..........................
297 *
298 * // Return to previous mode
299 * i18n_select($previous);
300 *
301 * @param $value
302 * Optional, enable/disable selection: TRUE/FALSE
303 * @return boolean
304 * Previous selection mode (TRUE/FALSE)
305 */
306 function i18n_select($value = NULL) {
307 static $mode = FALSE;
308
309 if (isset($value)) {
310 $previous = $mode;
311 $mode = $value;
312 return $previous;
313 }
314 else {
315 return $mode;
316 }
317 }
318
319 /**
320 * Get language properties.
321 *
322 * @param $code
323 * Language code.
324 * @param $property
325 * It may be 'name', 'native', 'ltr'...
326 */
327 function i18n_language_property($code, $property) {
328 $languages = language_list();
329 return isset($languages[$code]->$property) ? $languages[$code]->$property : NULL;
330 }
331
332 /**
333 * Implements hook_preprocess_html().
334 */
335 function i18n_preprocess_html(&$variables) {
336 global $language;
337 $variables['classes_array'][] = 'i18n-' . $language->language;
338 }
339
340 /**
341 * Translate or update user defined string. Entry point for i18n_string API if enabled.
342 *
343 * This function is from i18n_string sub module and is subject to be moved back.
344 *
345 * @param $name
346 * Textgroup and context glued with ':'.
347 * @param $default
348 * String in default language. Default language may or may not be English.
349 * @param $options
350 * An associative array of additional options, with the following keys:
351 * - 'langcode' (defaults to the current language) The language code to translate to a language other than what is used to display the page.
352 * - 'filter' Filtering callback to apply to the translated string only
353 * - 'format' Input format to apply to the translated string only
354 * - 'callback' Callback to apply to the result (both to translated or untranslated string
355 * - 'update' (defaults to FALSE) Whether to update source table
356 * - 'translate' (defaults to TRUE) Whether to return a translation
357 *
358 * @return $string
359 * Translated string, $string if not found
360 */
361 function i18n_string($name, $string, $options = array()) {
362 $options += array('translate' => TRUE, 'update' => FALSE);
363 if ($options['update']) {
364 $result = function_exists('i18n_string_update') ? i18n_string_update($name, $string, $options) : FALSE;
365 }
366 if ($options['translate']) {
367 $result = function_exists('i18n_string_translate') ? i18n_string_translate($name, $string, $options) : $string;
368 }
369 return $result;
370 }
371
372 /**
373 * Get object wrapper.
374 *
375 * Create an object wrapper or retrieve it from the static cache if
376 * a wrapper for the same object was created before.
377 *
378 * @see i18n_object_info()
379 *
380 * @param $type
381 * The object type.
382 */
383 function i18n_object($type, $object) {
384 $key = i18n_object_key($type, $object);
385 return i18n_get_object($type, $key, $object);
386 }
387
388 /**
389 * Get object wrapper by object key.
390 *
391 * @param $type
392 * The object type to load e.g. node_type, menu, taxonomy_term.
393 * @param $key
394 * The object key, can be an scalar or an array.
395 * @param $object
396 * Optional Drupal object or array. It will be autoloaded using the key if not present.
397 *
398 * @return
399 * A fully-populated object wrapper.
400 */
401 function i18n_get_object($type, $key, $object = NULL) {
402 $cache = &drupal_static(__FUNCTION__);
403 $index = is_array($key) ? implode(':', $key) : $key;
404 if (!isset($cache[$type][$index])) {
405 $class = i18n_object_info($type, 'class', 'i18n_object_wrapper');
406 $cache[$type][$index] = new $class($type, $key, $object);
407 }
408 return $cache[$type][$index];
409 }
410
411 /**
412 * Get object language code
413 *
414 * @param $object
415 * Object or array having language field or plain language field
416 * @param $default
417 * What value to return if the object doesn't have a valid language
418 */
419 function i18n_object_langcode($object, $default = FALSE, $field = 'language') {
420 $value = i18n_object_field($object, $field, $default);
421 return $value && $value != LANGUAGE_NONE ? $value : $default;
422 }
423
424 /**
425 * Get translation information for objects
426 */
427 function i18n_object_info($type = NULL, $property = NULL, $default = NULL) {
428 $info = &drupal_static(__FUNCTION__);
429 if (!$info) {
430 $info = module_invoke_all('i18n_object_info');
431 drupal_alter('i18n_object_info', $info);
432 }
433 if ($property) {
434 return isset($info[$type][$property]) ? $info[$type][$property] : $default;
435 }
436 elseif ($type) {
437 return isset($info[$type]) ? $info[$type] : array();
438 }
439 else {
440 return $info;
441 }
442 }
443
444 /**
445 * Get field value from object/array
446 */
447 function i18n_object_field($object, $field, $default = NULL) {
448 if (is_array($field)) {
449 // We can handle a list of fields too. This is useful for multiple keys (like blocks)
450 foreach ($field as $key => $name) {
451 $values[$key] = i18n_object_field($object, $name);
452 }
453 return $values;
454 }
455 elseif (strpos($field, '.')) {
456 // Access nested properties with the form 'name1.name2..', will map to $object->name1->name2...
457 $names = explode('.', $field);
458 $current = array_shift($names);
459 if ($nested = i18n_object_field($object, $current)) {
460 return i18n_object_field($nested, implode('.', $names), $default);
461 }
462 else {
463 return $default;
464 }
465 }
466 elseif (is_object($object)) {
467 return isset($object->$field) ? $object->$field : $default;
468 }
469 elseif (is_array($object)) {
470 return isset($object[$field]) ? $object[$field] : $default;
471 }
472 else {
473 return $default;
474 }
475 }
476
477 /**
478 * Get key value from object/array
479 */
480 function i18n_object_key($type, $object, $default = NULL) {
481 if ($field = i18n_object_info($type, 'key')) {
482 return i18n_object_field($object, $field, $default);
483 }
484 else {
485 return $default;
486 }
487 }
488
489 /**
490 * Menu access callback for mixed translation tab
491 */
492 function i18n_object_translate_access($type, $object) {
493 return i18n_object($type, $object)->get_translate_access();
494 }
495
496 /**
497 * Get translations for path.
498 *
499 * @param $path
500 * Path to get translations for or '<front>' for front page.
501 * @param $check_access
502 * Whether to check access to paths, defaults to TRUE
503 */
504 function i18n_get_path_translations($path, $check_access = TRUE) {
505 $translations = &drupal_static(__FUNCTION__);
506
507 if (!isset($translations[$path])) {
508 $translations[$path] = array();
509 foreach (module_implements('i18n_translate_path') as $module) {
510 $translated = call_user_func($module . '_i18n_translate_path', $path);
511 // Add into the array, if two modules returning a translation first takes precedence.
512 if ($translated) {
513 $translations[$path] += $translated;
514 }
515 }
516 // Add access information if not there.
517 foreach ($translations[$path] as $langcode => &$info) {
518 if (!isset($info['access'])) {
519 $item = menu_get_item($info['href']);
520 // If no menu item, it may be an external URL, we allow access.
521 $info['access'] = $item ? !empty($item['access']) : TRUE;
522 }
523 }
524 // Chance for altering the results.
525 drupal_alter('i18n_translate_path', $translations[$path], $path);
526 }
527
528 if ($check_access) {
529 return array_filter($translations[$path], '_i18n_get_path_translations_access');
530 }
531 else {
532 return $translations[$path];
533 }
534 }
535
536 /**
537 * Helper function to check access to path translation.
538 */
539 function _i18n_get_path_translations_access($path) {
540 return $path['access'];
541 }
542
543 /**
544 * Implements hook_language_switch_links_alter().
545 *
546 * Replaces links with pointers to translated versions of the content.
547 */
548 function i18n_language_switch_links_alter(array &$links, $type, $path) {
549 // For the front page we have nothing to add to Drupal core links.
550 if ($path != '<front>' && ($translations = i18n_get_path_translations($path))) {
551 foreach ($translations as $langcode => $translation) {
552 if (isset($links[$langcode])) {
553 $links[$langcode]['href'] = $translation['href'];
554 if (!empty($translation['title'])) {
555 $links[$langcode]['attributes']['title'] = $translation['title'];
556 }
557 }
558 }
559 }
560 }
561
562 /**
563 * Build translation link
564 */
565 function i18n_translation_link($path, $langcode, $link = array()) {
566 $language = i18n_language_object($langcode);
567 $link += array(
568 'href' => $path,
569 'title' => $language->native,
570 'language' => $language,
571 'i18n_translation' => TRUE,
572 );
573 $link['attributes']['class'] = array('language-link');
574 // @todo Fix languageicons weight, but until that
575 if (function_exists('languageicons_link_add')) {
576 languageicons_link_add($link);
577 }
578 return $link;
579 }
580
581 /**
582 * Implements hook_form_FORM_ID_alter().
583 */
584 function i18n_form_block_admin_display_form_alter(&$form, &$form_state) {
585 $form['#submit'][] = 'i18n_form_block_admin_display_form_submit';
586 }
587
588 /**
589 * Display a help message when enabling the language switcher block.
590 */
591 function i18n_form_block_admin_display_form_submit($form, &$form_state) {
592 foreach ($form_state['values']['blocks'] as $key => $block) {
593 $previous = $form['blocks'][$key]['region']['#default_value'];
594 if (empty($previous) && $block['region'] != -1 && $block['module'] == 'locale') {
595 $message = t('The language switcher will appear only after configuring <a href="!url">language detection</a>. You need to enable at least one method that alters URLs like <em>URL</em> or <em>Session</em>.', array('!url' => url('admin/config/regional/language/configure')));
596 drupal_set_message($message, 'warning', FALSE);
597 break;
598 }
599 }
600 }
601
602 /**
603 * Normal path should be checked with menu item's language to avoid
604 * troubles when a node and it's translation has the same url alias.
605 */
606 function i18n_prepare_normal_path($link_path, $language) {
607 $normal_path = drupal_get_normal_path($link_path, $language);
608 if ($link_path != $normal_path) {
609 drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $link_path, '%normal_path' => $normal_path)));
610 }
611 return $normal_path;
612 }
613
614 /**
615 * Checks if an entity translation is enabled for the given entity type.
616 * @param $entity_type
617 */
618 function i18n_entity_translation_enabled($entity_type) {
619 $cache = &drupal_static(__FUNCTION__);
620 if (!isset($cache[$entity_type])) {
621 // Check if the entity_translation module exists and if so if the given
622 // entity type is handled.
623 $cache[$entity_type] = module_exists('entity_translation') && entity_translation_enabled($entity_type);
624 }
625 return $cache[$entity_type];
626 }
627
628 /**
629 * Implements hook_modules_enabled().
630 */
631 function i18n_modules_enabled($modules) {
632 drupal_static_reset('i18n_object_info');
633 }