You are here

i18n.module in Internationalization 5.3

Same filename and directory in other branches
  1. 5 i18n.module
  2. 5.2 i18n.module
  3. 6 i18n.module
  4. 7 i18n.module

Internationalization (i18n) module

@author Jose A. Reyero, 2004

File

i18n.module
View source
<?php

/**
 * @file
 * Internationalization (i18n) module
 *
 * @author Jose A. Reyero, 2004
 *
 */

// Some constants. Language support modes for content
define('LANGUAGE_SUPPORT_NONE', 0);
define('LANGUAGE_SUPPORT_NORMAL', 1);
define('LANGUAGE_SUPPORT_EXTENDED', 2);

/**
 * Module initialization
 * 
 * Get language from path if exists and Initialize i18n system
 *
 * We allow other module to do a previous include and override all
 * of i18n's path handling functions and hooks, that's why many of them are wrapped
 * with conditional function_exists()
 * 
 * This will be used for integration with 'context' module.
 */

/**
 * This one expects to be called first from common.inc
 */
if (!function_exists('i18n_get_lang')) {
  function i18n_get_lang($setlanguage = NULL) {
    static $i18n_language;

    //see if the language is already set.
    if ($setlanguage) {
      $i18n_language = $setlanguage;
    }
    elseif ($i18n_language) {
      return $i18n_language;
    }
    else {
      _i18n_init();
      return $i18n_language = _i18n_get_lang();
    }
  }
}

/**
 *  Gets language, checking in order:
 *
 *  1. Path language
 *  2. Session language
 *  3. User language
 *  4. Browser language
 *  5. Default language
 */
if (!function_exists('_i18n_get_lang')) {
  function _i18n_get_lang() {
    global $user, $i18n_langpath;
    static $i18n_lang;

    // Check whether the language is already set.
    if ($i18n_lang) {
      return $i18n_lang;
    }

    // Language not set, find one
    $languages = i18n_supported_languages();
    if ($i18n_langpath && array_key_exists($i18n_langpath, $languages)) {

      // Sets session language only when language is in path, not for other cases
      $_SESSION['language'] = $i18n_lang = $i18n_langpath;
    }
    elseif (isset($_SESSION['language']) && array_key_exists($_SESSION['language'], $languages)) {
      $i18n_lang = $_SESSION['language'];
    }
    elseif ($user->uid && $user->language && array_key_exists($user->language, $languages)) {
      $i18n_lang = $user->language;
    }
    elseif (variable_get("i18n_browser", 0) && ($lang = i18n_get_browser_lang())) {
      $i18n_lang = $lang;
    }
    else {
      $i18n_lang = i18n_default_language();
    }
    return $i18n_lang;
  }
}

/**
 * Minimum initialization
 */
if (!function_exists('_i18n_init')) {
  function _i18n_init() {
    global $i18n_langpath;
    $path = _i18n_get_original_path();
    $i18n_langpath = i18n_get_lang_prefix($path);
  }
}

/**
 * Language block
 * 
 * This is a simple language switcher which knows nothing about translations
 */
function i18n_block($op = 'list', $delta = 0) {
  if ($op == 'list') {
    $blocks[0]['info'] = t('Language switcher (Interface only)');
  }
  elseif ($op == 'view') {
    $blocks['subject'] = t('Languages');
    $query = drupal_query_string_encode($_GET, array(
      'q',
    ));
    $blocks['content'] = theme('item_list', i18n_get_links($_GET['q'], empty($query) ? NULL : $query));
  }
  return $blocks;
}

/**
 * Implementation of hook_init()
 * 
 * May do a redirect from home page for not to get wrong versions in cache
 * Warning: when in bootstrap mode, this may be called before i18n_get_lang()
 */
function i18n_init() {
  global $i18n_langpath;

  // If not in bootstrap, variable init. This may go before the rest of the code because some variables may need to be loaded
  if (!_i18n_is_bootstrap()) {

    //include drupal_get_path('module', 'i18n').'/i18n.inc';
    i18n_variable_init();
  }
  $lang = i18n_get_lang();
  $path = _i18n_get_original_path();

  // Init selection mode
  i18n_selection_mode(variable_get('i18n_selection_mode', 'simple'));

  // Multi tables, for backwards compatibility and experimentation
  _i18n_set_db_prefix($lang);
  if ($path == '') {

    // Main page
    // Check for update or cron scripts to disable rewriting and redirection
    if (preg_match('|/(?!index\\.php)\\w+\\.php|', request_uri())) {
      i18n_selection_mode('off');
    }
    elseif ($lang != i18n_default_language()) {

      // Redirect to main page in $lang when it's not default language.
      _i18n_goto($lang);
    }
    else {
      $_GET['q'] = i18n_frontpage($lang);
    }
  }
  elseif ($lang == $path) {

    // When path is only language code
    $_GET['q'] = i18n_frontpage($lang);
  }
  elseif ($i18n_langpath) {

    //search alias with and without lang and remove lang.
    $_GET['q'] = i18n_get_normal_path($path);
  }
}

/**
 * Implementation of hook_help().
 */
function i18n_help($section = 'admin/help#i18n') {
  switch ($section) {
    case 'admin/help#i18n':
      $output = '<p>' . t('This module provides support for multilingual content in Drupal sites:') . '</p>';
      $output .= '<ul>';
      $output .= '<li>' . t('Translation of the user interface for anonymous users (combined with locale)') . '</li>';
      $output .= '<li>' . t('Multi-language for content. Adds a language field for nodes and taxonomy vocabularies and terms') . '</li>';
      $output .= '<li>' . t('Browser language detection') . '</li>';
      $output .= '<li>' . t('Keeps the language setting accross consecutive requests using URL rewriting') . '</li>';
      $output .= '<li>' . t('Provides a block for language selection and two theme functions: <i>i18n_flags</i> and <i>i18n_links</i>') . '</li>';
      $output .= '<li>' . t('Support for long locale names') . '</li>';
      $output .= '<li>' . t('Multilingual menu items') . '</li>';
      $output .= '</ul>';
      $output .= '<p>' . t('For more information please read the <a href="@i18n">on-line help pages</a>.', array(
        '@i18n' => 'http://drupal.org/node/133977',
      )) . '</p>';
      return $output;
    case 'admin/settings/i18n':
      $output .= '<p>' . t('To enable multilingual support for specific content types go to !configure_content_types.', array(
        '!configure_content_types' => l(t('configure content types'), 'admin/content/types'),
      )) . '</p>';
      return $output;
  }
}

/**
 * Implementation of hook_menu().
 * Modify rewriting conditions when viewing specific nodes
 */
function i18n_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/i18n',
      'title' => t('Multilingual system'),
      'description' => t('Configure multilingual content and translation.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'i18n_admin_settings',
      ),
      'access' => user_access('administer site configuration'),
    );
    $items[] = array(
      'path' => 'admin/settings/i18n/main',
      'title' => t('Internationalization'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/i18n/language',
      'title' => t('Manage languages'),
      'description' => t('Configure languages.'),
      'callback' => 'locale_admin_manage',
      'type' => MENU_LOCAL_TASK,
    );
  }
  else {
    if (arg(0) == 'node') {
      if (isset($_POST['language']) && $_POST['language']) {
        $language = $_POST['language'];
      }
      elseif (is_numeric(arg(1)) && ($node = node_load(arg(1)))) {

        // Node language when loading specific nodes
        $language = $node->language;
      }
      if ($language) {
        i18n_selection_mode('node', db_escape_string($language));
      }
    }
    elseif (arg(0) == 'admin' && arg(0) == 'content' && user_access('administer all languages')) {

      // No restrictions for administration pages
      i18n_selection_mode('off');
    }
  }
  return $items;
}

/**
 * Implementation of hook_nodeapi().
 */
function i18n_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  if (variable_get("i18n_node_{$node->type}", 0)) {
    switch ($op) {
      case 'load':
        return db_fetch_array(db_query("SELECT trid, language, status AS i18n_status FROM {i18n_node} WHERE nid=%d", $node->nid));
      case 'insert':
      case 'update':
        db_query("DELETE FROM {i18n_node} WHERE nid=%d", $node->nid);
        if ($node->language) {

          // Assign a trid from the beginning
          db_query("INSERT INTO {i18n_node} (nid, trid, language, status) VALUES(%d, '%d', '%s', '%d')", $node->nid, $node->trid, $node->language, $node->i18n_status);
        }

        // Handle menu items. Fixes duplication issue and language for menu items which happens when editing nodes in languages other than current.
        if (isset($node->menu) && !$node->menu['delete'] && $node->menu['title']) {
          $item = $node->menu;
          $item['path'] = $item['path'] ? $item['path'] : "node/{$node->nid}";
          $item['type'] = $item['type'] | MENU_MODIFIED_BY_ADMIN;
          if ($item['mid']) {

            // Update menu item
            db_query("UPDATE {menu} SET pid = %d, path = '%s', title = '%s', description = '%s', weight = %d, type = %d, language = '%s' WHERE mid = %d", $item['pid'], $item['path'], $item['title'], $item['description'], $item['weight'], $item['type'], $node->language, $item['mid']);
            drupal_set_message(t('The menu item %title has been updated with node language.', array(
              '%title' => $item['title'],
            )));
          }
          elseif (SAVED_NEW == menu_save_item($item)) {

            // Creating new menu item with node language
            db_query("UPDATE {menu} SET language = '%s' WHERE mid = %d", $node->language, $item['mid']);
            drupal_set_message(t('The menu item %title has been added with node language.', array(
              '%title' => $item['title'],
            )));
          }
          menu_rebuild();
          unset($node->menu);

          // Avoid further processing by menu module
        }

        // Pathauto integration. Dynamic replacement of variables to allow different patterns per language
        if (module_exists('path') && module_exists('pathauto')) {

          // Language for pathauto variables is either node language or default language
          $language = $node->language ? $node->language : i18n_default_language();
          if ($language != i18n_get_lang()) {
            i18n_variable_init($language, 'pathauto_node');
          }
        }
        break;
      case 'delete':
        db_query('DELETE FROM {i18n_node} WHERE nid=%d', $node->nid);
        break;
      case 'prepare':

        // Book pages, set the right language nodes and outlines
        if (arg(3) == 'parent' && is_numeric(arg(4)) && ($parent = node_load(arg(4))) && $parent->language) {
          $node->language = $parent->language;
          i18n_selection_mode('node', $parent->language);
        }
        break;
    }
  }
}

/**
 * Implementation of hook_taxonomy
 * 
 * $edit parameter may be an array or an object !!
 */
function i18n_taxonomy($op, $type, $edit = NULL) {
  $edit = (array) $edit;
  switch ("{$type}/{$op}") {
    case 'term/insert':
    case 'term/update':
      $language = isset($edit['language']) ? $edit['language'] : '';
      db_query("UPDATE {term_data} SET language='%s' WHERE tid=%d", $language, $edit['tid']);
      break;
    case 'vocabulary/insert':
    case 'vocabulary/update':
      $language = isset($edit['language']) ? $edit['language'] : '';
      db_query("UPDATE {vocabulary} SET language='%s' WHERE vid=%d", $language, $edit['vid']);
      if ($language && $op == 'update') {
        db_query("UPDATE {term_data} SET language='%s' WHERE vid=%d", $edit['language'], $edit['vid']);
        drupal_set_message(t('Reset language for all terms.'));
      }
      break;
  }
}

/**
 * Implementation of hook_user()
 * 
 * Switch to user's language after login
 */
function i18n_user($op, &$edit, &$account, $category = NULL) {
  if ($op == 'login' && $account->language) {
    $_SESSION['language'] = $account->language;
    i18n_get_lang($account->language);
  }
}

/**
 * Form builder function.
 * 
 * Some options have been removed from previous versions:
 * - Languages are now taken from locale module unless defined in settings file
 * - Language dependent tables are authomatically used if defined in settings file
 */
function i18n_admin_settings() {

  // Check languages
  $languages = variable_get('i18n_languages', array());
  if (!count($languages) || !count($languages['active']) > 1) {
    drupal_set_message(t('No languages enabled. Visit the !locale_admin page to set up your languages.', array(
      '!locale_admin' => l(t('Manage languages'), 'admin/settings/locale/'),
    )), 'error');
  }
  $form['i18n_browser'] = array(
    '#type' => 'radios',
    '#title' => t('Browser language detection'),
    '#default_value' => variable_get('i18n_browser', 0),
    '#options' => array(
      t('Disabled'),
      t('Enabled'),
    ),
    '#description' => t("User browser language for home page and links without language prefix."),
  );

  // Content selection mode
  $form['i18n_selection_mode'] = array(
    '#type' => 'radios',
    '#title' => t('Content selection mode'),
    '#default_value' => variable_get('i18n_selection_mode', 'simple'),
    '#options' => _i18n_selection_mode(),
    '#description' => t('Determines which content to show depending on the current page language and the default language of the site.'),
  );

  // Language icons
  $form['icons'] = array(
    '#type' => 'fieldset',
    '#title' => t('Language icons settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['icons']['i18n_icon_path'] = array(
    '#type' => 'textfield',
    '#title' => t('Language icons path'),
    '#default_value' => variable_get('i18n_icon_path', drupal_get_path('module', 'i18n') . '/flags/*.png'),
    '#size' => 70,
    '#maxlength' => 180,
    '#description' => t('Path for language icons, relative to Drupal installation. \'*\' is a placeholder for language code.'),
  );
  $form['icons']['i18n_icon_size'] = array(
    '#type' => 'textfield',
    '#title' => t('Language icons size'),
    '#default_value' => variable_get('i18n_icon_size', '16x12'),
    '#size' => 10,
    '#maxlength' => 10,
    '#description' => t('Image size for language icons, in the form "width x height".'),
  );
  return system_settings_form($form);
}

/**
 * Simple i18n API
 */

/**
 * Get localized language list, sort alphabetically
 * 
 * @param $all
 *   TRUE for all languages, not only enabled
 */
function i18n_language_list($all = FALSE) {
  static $languages;
  $type = $all ? 'name' : 'active';
  if (!isset($languages[$type])) {
    $languages[$type] = array_map('t', i18n_languages($type));
    asort($languages[$type]);
    return $languages[$type];
  }
  return $languages[$type];
}

/**
 * Get list of supported languages
 * @param $all
 *   TRUE to get all defined languages
 */
function i18n_supported_languages($all = FALSE) {
  return i18n_languages($all ? 'name' : 'active');
}

/**
 * Returns default language code
 */
function i18n_default_language() {
  return i18n_languages('site_default');
}

/**
 * Check whether language is RTL
 * 
 * @param $language
 *   Language to check, defaults to current language
 */
function i18n_language_rtl($language = NULL) {
  $language = $language ? $language : i18n_get_lang();
  return i18n_language_property($language, 'rtl') ? TRUE : FALSE;
}

/**
 * Get language properties
 * 
 * @param $code
 *   Language code
 * @param $property
 *   It may be 'name', 'native', 'ltr'...
 */
function i18n_language_property($code, $property = 'native') {
  $languages = i18n_languages($property);
  return $languages[$code];
}

/**
 * Get locale languages plus i18n language settings
 * 
 * @param $key
 *   Data to be returned, defaults to 'active'
 *   'active' => array of enabled languages with native name
 *   'enabled' => array of enabled languages with english name
 *   'name' => all languages defined in locale module with english name
 *   'site_default' => code of default site language
 */
function i18n_languages($key = 'active') {
  static $languages;
  if (!$languages) {
    if ($languages = variable_get('i18n_languages', 0)) {
      foreach ($languages['name'] as $code => $name) {
        if ($languages['enabled'][$code]) {
          $languages['active'][$code] = $name;
        }
      }
    }
    else {

      // It is possible that languages are not initialized
      if (function_exists('locale_supported_languages')) {
        $languages = locale_supported_languages();
      }
      else {

        // Worst case scenario: locale module not loaded, for cached pages, at least this won't break everything
        unset($languages);

        // There's some PHP bug: http://www.zend.com/zend/week/week98.php
        $languages['name'] = array(
          'en' => 'English',
        );
      }
      $languages['site_default'] = key($languages['name']);
      $languages['active'] = $languages['name'];
      $languages['native'] = $languages['name'];
    }

    // Sort everything alphabetically
    asort($languages['name']);
    asort($languages['active']);
    asort($languages['native']);
  }
  return $key ? $languages[$key] : $languages;
}

/**
 * Get language from browser settings, but only if it is a valid language
 */
function i18n_get_browser_lang() {
  $languages = i18n_supported_languages();
  $exploded_server = explode(";", $_SERVER["HTTP_ACCEPT_LANGUAGE"]);
  $accept = explode(',', array_shift($exploded_server));
  foreach ($accept as $lang) {
    if (empty($lang)) {
      continue;
    }
    elseif (array_key_exists($lang, $languages)) {
      return $lang;
    }
    elseif (array_key_exists(substr($lang, 0, 2), $languages)) {
      return substr($lang, 0, 2);
    }
  }
}

/**
 * Get language code from path.
 *
 * @param $path
 * @param $trim
 *   TRUE to remove language code from $path
 */
function i18n_get_lang_prefix(&$path, $trim = FALSE) {
  $exploded_path = explode('/', $path);
  $maybelang = array_shift($exploded_path);
  $languages = i18n_languages();
  if (array_key_exists($maybelang, $languages)) {
    if ($trim) {
      $path = trim(substr($path, strlen($maybelang)), '/');
    }
    return $maybelang;
  }
}

/**
 * Language dependent front page
 * This function will search for aliases like 'en/home', 'es/home'...
 */
function i18n_frontpage($lang = NULL) {
  $lang = $lang ? $lang : _i18n_get_lang();
  return i18n_get_normal_path($lang . '/' . variable_get('site_frontpage', 'node'));
}

/**
 * This function is similar to drupal_get_normal_path, but language-aware
 * Also removes language from path
 */
function i18n_get_normal_path($path) {
  $prefix = i18n_get_lang_prefix($path, TRUE);
  if (!$prefix || _i18n_is_bootstrap()) {

    // If bootstrap, drupal_lookup_path is not defined
    return $path;
  }
  elseif ($alias = drupal_lookup_path('source', $prefix . '/' . $path)) {
    i18n_get_lang_prefix($alias, TRUE);

    // In case alias has language
    return $alias;
  }
  elseif ($alias = drupal_lookup_path('source', $path)) {
    i18n_get_lang_prefix($alias, TRUE);
    return $alias;
  }
  else {
    return $path;
  }
}

/**
 * More i18n API
 */

/**
 * Produces i18n paths, with language prefix
 * If path is empty or site frontpage, path = 'lang'
 * Check for frontpage and search for alias before adding language
 */
function i18n_path($path, $lang) {
  if (!$path || $path == i18n_frontpage($lang)) {
    return $lang;
  }
  elseif ($alias = drupal_lookup_path('alias', $path)) {
    if ($prefix = i18n_get_lang_prefix($alias)) {

      // This alias will be valid only if it has the same language
      return $prefix == $lang ? $alias : $lang . '/' . $path;
    }
    else {

      // Alias without language prefix
      return $lang . '/' . $alias;
    }
  }
  else {

    // Alias for language path will be searched later
    return $lang . '/' . $path;
  }
}

/**
 * Get node language
 */
function i18n_node_get_lang($nid, $default = '') {
  $lang = db_result(db_query('SELECT language FROM {i18n_node} WHERE nid=%d', $nid));
  return $lang ? $lang : $default;
}

/**
 * Get allowed languages for node
 * 
 * This allows node types to define its own language list implementing hook 'language_list'
 */
function i18n_node_language_list($node) {
  if ($languages = node_invoke($node, 'language_list')) {
    return $languages;

    // The node module manages its own language list
  }
  elseif (variable_get('i18n_node_' . $node->type, 0) == LANGUAGE_SUPPORT_EXTENDED) {
    return i18n_language_list(TRUE);

    // All defined languages
  }
  else {
    return i18n_language_list();

    // All enabled languages
  }
}

/**
 * Returns main language, two letter code
 */
function i18n_get_main_lang($lang = NULL) {
  $lang = $lang ? $lang : i18n_get_lang();
  return substr($lang, 2);
}

/**
 * Function i18n_get_links
 * 
 * Returns an array of links for all languages, with or without names/flags
 * 
 * @param $path
 *   Drupal internal path
 * @param $query
 *   Query string
 * @param $names
 *   Names to use for the links. Defaults to native language names
 */
function i18n_get_links($path = '', $query = NULL, $names = NULL) {
  if ($path == i18n_frontpage()) {
    $path = '';
  }
  $names = $names ? $names : i18n_languages('native');
  foreach (array_keys(i18n_supported_languages()) as $lang) {
    $links[$lang] = theme('i18n_link', $names[$lang], i18n_path($path, $lang), $lang, $query);
  }
  return $links;
}

/**
 * Returns list of enabled languages from locale module
 *
 * Some code borrowed from locale module.
 * And yes, if locale enabled, languages are cached twice. But better twice than never ;-)
 */
function _i18n_locale_supported_languages() {
  if (function_exists('locale_supported_languages')) {
    $languages = locale_supported_languages();
    return $languages['name'];
  }
  else {
    $result = db_query('SELECT locale, name FROM {locales_meta} WHERE enabled = 1 ORDER BY isdefault DESC, name ASC');
    while ($row = db_fetch_object($result)) {
      $enabled[$row->locale] = $row->name;
    }
    return $enabled;
  }
}

/**
 * Emulates drupal_goto, it may not be loaded yet
 */
function _i18n_goto($lang) {
  if (!function_exists('drupal_goto')) {
    require_once './includes/common.inc';
    require_once './includes/path.inc';
  }
  drupal_goto($lang);
}

/**
 * i18n_selection_mode
 * 
 * Allows several modes for query rewriting and to change them programatically
 * 	off = No language conditions inserted
 * 	simple = Only current language and no language
 * 	mixed = Only current and default languages
 *      preferred = Current language or else (if available) default language, and no language
 *  strict = Only current language
 *  default = Only default language
 *  user = User defined, in the module's settings page
 *  params = Gets the stored params
 *  reset = Returns to previous
 *  custom = add custom where clause, like "%alias.language = 'en'"
 */
function i18n_selection_mode($mode = NULL, $params = NULL) {
  static $current_mode = 'simple';
  static $current_value = '';
  static $store = array();
  if (!$mode) {
    return $current_mode;
  }
  elseif ($mode == 'params') {
    return $current_value;
  }
  elseif ($mode == 'reset') {
    list($current_mode, $current_value) = array_pop($store);

    //drupal_set_message("i18n mode reset mode=$current_mode value=$current_value");
  }
  else {
    array_push($store, array(
      $current_mode,
      $current_value,
    ));
    $current_mode = $mode;
    $current_value = $params;
  }
}

// List of selection modes
function _i18n_selection_mode() {
  return array(
    // Emphasize the first 2 ones, will be the most preferred.
    'simple' => '<strong>' . t('Current language and no language') . '</strong>',
    'mixed' => '<strong>' . t('Current language (if available) or default language, and no language') . '</strong>',
    'default' => t('Only default language and no language'),
    'strict' => t('Only current language'),
    'off' => t('All content. No language conditions apply'),
  );
}

// List of language support modes for content
function _i18n_content_languages() {
  return array(
    LANGUAGE_SUPPORT_NONE => t('Disabled'),
    LANGUAGE_SUPPORT_NORMAL => t('Normal - All enabled languages will be allowed.'),
    LANGUAGE_SUPPORT_EXTENDED => t('Extended - All defined languages will be allowed.'),
  );
}

/**
 * @name Themeable functions
 * @{
 */

/**
 * Produces a language link with the right flag
 */
function theme_i18n_link($text, $target, $lang, $query = NULL, $fragment = NULL) {
  $output = '<span class="i18n-link">';
  $attributes = $lang == i18n_get_lang() ? array(
    'class' => 'active',
  ) : NULL;
  $output .= l(theme('i18n_language_icon', $lang), $target, $attributes, $query, $fragment, FALSE, TRUE);
  $output .= "&nbsp;";
  $output .= l($text, $target, $attributes, $query, $fragment, FALSE, TRUE);
  $output .= '</span>';
  return $output;
}

/**
 * Theme language icon
 * 
 * This function can be overridden for no language icons
 */
function theme_i18n_language_icon($lang) {
  if ($path = variable_get('i18n_icon_path', drupal_get_path('module', 'i18n') . '/flags/*.png')) {
    $languages = i18n_supported_languages();
    $src = base_path() . str_replace('*', $lang, $path);
    list($width, $height) = explode('x', variable_get('i18n_icon_size', '16x12'));
    $attribs = array(
      'class' => 'i18n-icon',
      'width' => $width,
      'height' => $height,
      'alt' => $languages[$lang],
    );
    return "<img src=\"{$src}\" " . drupal_attributes($attribs) . " />";
  }
}

/* @} */

/**
 * Implementation of conf_url_rewrite
 * 
 * This is a conditional definition, just in case it is defined somewhere else.
 * If so, path rewriting won't work properly but at least it won't break Drupal
 */
if (!function_exists('custom_url_rewrite')) {
  function custom_url_rewrite($type, $path, $original) {
    return i18n_url_rewrite($type, $path, $original);
  }
}

/**
 * Rewrites path with current language and removes prefix if searching for source path
 */
function i18n_url_rewrite($type, $path, $original) {
  if ($type == 'alias' && !strpos($path, '://') && !i18n_get_lang_prefix($path)) {
    return $path ? i18n_get_lang() . '/' . $path : i18n_get_lang();
  }
  elseif ($type == 'source') {
    if ($path == $original) {
      return i18n_get_normal_path($path);
    }
    else {

      // Path may have been dealiased but still have language prefix
      i18n_get_lang_prefix($path, TRUE);
      return $path;
    }
  }
  else {
    return $path;
  }
}

/**
 * Implementation of hook_db_rewrite_sql()
 */
function i18n_db_rewrite_sql($query, $primary_table, $primary_key) {

  // Some exceptions for query rewrites
  $mode = i18n_selection_mode();

  // drupal_set_message("i18n_db_rewrite mode=$mode query=$query");
  if ($mode == 'off') {
    return;
  }

  // Special case. Selection mode is 'strict' but this should be only for node queries
  if ($mode == 'strict' && $primary_table != 'n' && $primary_table != 'node') {
    $mode = 'simple';
  }
  switch ($primary_table) {
    case 'n':
    case 'node':
    case 'nc':

      // For node_comment table, added in 5.2
      // Node queries.
      // When loading specific nodes, language conditions shouldn't apply
      if (preg_match("/WHERE.*\\s{$primary_table}.nid\\s*=\\s*(\\d|%d)/", $query)) {
        return;
      }

      // If language conditions already there, get out
      if (preg_match("/i18n/", $query)) {
        return;
      }
      $result['join'] = "LEFT JOIN {i18n_node} i18n ON {$primary_table}.nid = i18n.nid";

      // For 'mixed mode' and nodes we need to join in an aditional table and apply special conditions
      if ($mode == 'mixed') {
        $result['where'] = i18n_db_rewrite_where('i18n', 'simple');
        if (i18n_get_lang() != i18n_default_language()) {
          $result['join'] .= " LEFT JOIN {i18n_node} i18n2 ON i18n.trid = i18n2.trid AND i18n2.language = '" . i18n_get_lang() . "'";
          $result['where'] .= " OR (i18n.language = '" . i18n_default_language() . "' AND i18n2.nid IS NULL)";
        }
      }
      else {
        $result['where'] = i18n_db_rewrite_where('i18n', $mode);
      }
      return $result;
    case 't':
    case 'v':

      // Taxonomy queries
      // When loading specific terms, vocabs, language conditions shouldn't apply
      if (preg_match("/WHERE.* {$primary_table}\\.tid\\s*(=\\s*\\d|IN)/", $query)) {
        return;
      }

      // Taxonomy for specific node
      if (preg_match("/WHERE r\\.nid = \\%d/", $query)) {
        return;
      }
      $result['where'] = i18n_db_rewrite_where($primary_table, $mode);
      return $result;
    case 'm':

      // Should not apply for menu administration pages
      if (arg(0) == 'admin' && arg(1) == 'build' && arg(2) == 'menu') {
        return;
      }

      // Menu queries. Rewrite mode is always 'simple' to avoid trouble with cache
      $result['where'] = i18n_db_rewrite_where($primary_table, 'simple');
      return $result;
  }
}

/**
 * Rewrites queries depending on rewriting mode
 */
function i18n_db_rewrite_where($alias, $mode) {

  // Get languages to simplify query building.
  $current = i18n_get_lang();
  $default = i18n_default_language();

  // If mode is mixed but current = default, it is the same as 'simple'.
  if ($mode == 'mixed' && $current == $default) {
    $mode = 'simple';
  }
  switch ($mode) {
    case 'simple':
      return "{$alias}.language ='{$current}' OR {$alias}.language ='' OR {$alias}.language IS NULL";
    case 'mixed':

      // This will be only for objects other than nodes
      return "{$alias}.language = '{$current}' OR {$alias}.language = '{$default}' OR {$alias}.language = '' OR {$alias}.language IS NULL";
    case 'strict':
      return "{$alias}.language ='{$current}'";
    case 'node':
    case 'translation':
      return "{$alias}.language ='" . i18n_selection_mode('params') . "' OR {$alias}.language ='' OR {$alias}.language IS NULL";
    case 'default':
      return "{$alias}.language ='{$default}' OR {$alias}.language ='' OR {$alias}.language IS NULL";
    case 'custom':
      return str_replace('%alias', $alias, i18n_selection_mode('params'));
  }
}

/**
 * Implementation of hook_exit
 */
function i18n_exit() {
  _i18n_variable_exit();
}

/**
 * Implementation of hook_form_alter
 * 
 * This is the place to add language fields to all forms
 */
function i18n_form_alter($form_id, &$form) {

  //drupal_set_message("DEBUG: i18n_form_alter form_id=$form_id ");
  switch ($form_id) {
    case 'taxonomy_overview_vocabularies':
      $vocabularies = taxonomy_get_vocabularies();
      $languages = i18n_supported_languages();
      foreach ($vocabularies as $vocabulary) {
        if ($vocabulary->language) {
          $form[$vocabulary->vid]['type']['#value'] = $form[$vocabulary->vid]['type']['#value'] . '&nbsp(' . $languages[$vocabulary->language] . ')';
        }
      }
      break;
    case 'taxonomy_form_vocabulary':

      // Taxonomy vocabulary
      if (isset($form['vid'])) {
        $vocabulary = taxonomy_get_vocabulary($form['vid']['#value']);
      }
      $form['i18n'] = array(
        '#type' => 'fieldset',
        '#title' => t('Multilingual options'),
        '#collapsible' => TRUE,
        '#weight' => -20,
      );
      $form['i18n']['language'] = _i18n_language_select(isset($vocabulary) ? $vocabulary->language : i18n_get_lang(), t('This language will be set for all terms in this vocabulary'));
      break;
    case 'taxonomy_form_term':

      // Taxonomy term
      if (isset($form['tid']) && is_numeric($form['tid']['#value'])) {
        $term = taxonomy_get_term($form['tid']['#value']);
      }
      $form['language'] = _i18n_language_select(isset($term) ? $term->language : i18n_get_lang());
      break;
    case 'node_type_form':
      $node_type = $form['old_type']['#value'];

      // Build a fieldset to allow some more options here
      $form['workflow']['i18n'] = array(
        '#type' => 'fieldset',
        '#title' => t('Multilingual options'),
      );
      $form['workflow']['i18n']['i18n_node'] = array(
        '#type' => 'radios',
        '#title' => t('Multilingual content'),
        '#default_value' => variable_get('i18n_node_' . $node_type, 0),
        '#options' => _i18n_content_languages(),
        '#description' => t('Enables language field and translations for this content type.'),
      );
      break;
    case '_locale_admin_manage_screen':

      // Locale language settings
      $languages = locale_supported_languages(TRUE, TRUE);
      $i18nlangs = variable_get('i18n_languages', array());
      $form['native'] = array(
        '#tree' => TRUE,
      );
      foreach ($languages['name'] as $key => $lang) {
        $options[$key] = '';
        $form['native'][$key] = array(
          '#type' => 'textfield',
          '#default_value' => $i18nlangs['native'][$key] ? $i18nlangs['native'][$key] : $lang,
          '#size' => 15,
          '#maxlength' => 64,
        );
      }
      $form['rtl'] = array(
        '#type' => 'checkboxes',
        '#options' => $options,
        '#default_value' => $i18nlangs['rtl'],
      );
      $form['#base'] = 'i18n_admin_manage_screen';
      if (!is_array($form['#submit'])) {
        $form['#submit'] = array(
          '_locale_admin_manage_screen_submit' => array(),
        );
      }
      $form['#submit'] = array(
        'i18n_admin_manage_screen_submit' => array(),
      ) + $form['#submit'];
      break;
    case 'menu_edit_item_form':
      if ($mid = $form['mid']['#value']) {
        $language = db_result(db_query('SELECT language FROM {menu} WHERE mid = %d', $mid));
      }
      else {
        $language = '';
      }
      $form['language'] = _i18n_language_select($language, t('You can set a language for this menu item.'));
      $form['#submit'] = array(
        'i18n_menu_edit_item_form_submit' => array(),
      );
      break;
    default:

      // Node edit form
      if (isset($form['type']) && $form['type']['#value'] . '_node_form' == $form_id && ($node = $form['#node'])) {

        // Language field
        if (variable_get('i18n_node_' . $form['type']['#value'], 0) && !isset($form['i18n']['language'])) {

          // Language field
          $form['i18n'] = array(
            '#type' => 'fieldset',
            '#title' => t('Multilingual settings'),
            '#collapsible' => TRUE,
            '#collapsed' => FALSE,
            '#weight' => -4,
          );

          // Language will default to current only when creating a node
          $language = isset($form['#node']->language) ? $form['#node']->language : (arg(1) == 'add' ? i18n_get_lang() : '');
          $form['i18n']['language'] = _i18n_language_select($language, t('If you change the Language, you must click on <i>Preview</i> to get the right Categories &amp; Terms for that language.'), -4, i18n_node_language_list($node));
          $form['i18n']['trid'] = array(
            '#type' => 'value',
            '#value' => $form['#node']->trid,
          );
        }

        // Correction for lang/node/nid aliases generated by path module
        // if($form['#node']->path && $form['#node']->path == i18n_get_lang().'/node/'.$form['#node']->nid){
        if ($node->path) {
          $alias = drupal_lookup_path('alias', 'node/' . $node->nid);
          if ($alias && $alias != 'node/' . $node->nid) {
            $form['#node']->path = $alias;
          }
          else {
            unset($form['#node']->path);
          }
        }
      }

      // Multilingual variables in settings form
      if ($form['#base'] == 'system_settings_form' && ($variables = variable_get('i18n_variables', 0))) {
        if (i18n_form_alter_settings($form, $variables)) {
          $form['#submit'] = array(
            'i18n_variable_form_submit' => array(),
          );
        }
      }
  }
}

/**
 * Implementation of hook_perm().
 * 
 * Permissions defined
 * - administer all languages
 *   Disables language conditions for administration pages, so the user can view objects for all languages at the same time.
 *   This applies for: menu items, taxonomy
 */
function i18n_perm() {
  return array(
    'administer all languages',
  );
}

/**
 * Implementation of hook_token_values().
 */
function i18n_token_values($type, $object = NULL) {
  $values = array();
  switch ($type) {
    case 'node':
    case 'taxonomy':
      $values['lang'] = i18n_node_get_lang($object->nid, '');
      break;
  }
  return $values;
}

/**
 * Implementation of hook_token_list().
 */
function i18n_token_list($type = 'all') {
  $tokens = array();
  if ($type == 'node' || $type == 'all') {
    $tokens['node']['lang'] = t("Language code of the document.");
  }
  if ($type == 'taxonomy' || $type == 'all') {
    $tokens['taxonomy']['lang'] = t("Language code of the document.");
  }
  return $tokens;
}

/**
 * Process menu and menu item add/edit form submissions.
 */
function i18n_menu_edit_item_form_submit($form_id, $form_values) {
  $mid = menu_edit_item_save($form_values);
  db_query("UPDATE {menu} SET language = '%s' WHERE mid = %d", $form_values['language'], $mid);
  return 'admin/build/menu';
}

/**
 * Theme the locale admin manager form.
 */
function theme_i18n_admin_manage_screen($form) {
  foreach ($form['name'] as $key => $element) {

    // Do not take form control structures.
    if (is_array($element) && element_child($key)) {
      $rows[] = array(
        check_plain($key),
        drupal_render($form['name'][$key]),
        drupal_render($form['native'][$key]),
        drupal_render($form['enabled'][$key]),
        drupal_render($form['site_default'][$key]),
        drupal_render($form['rtl'][$key]),
        $key != 'en' ? drupal_render($form['translation'][$key]) : t('n/a'),
        $key != 'en' ? l(t('delete'), 'admin/settings/locale/language/delete/' . $key) : '',
      );
    }
  }
  $header = array(
    array(
      'data' => t('Code'),
    ),
    array(
      'data' => t('English name'),
    ),
    array(
      'data' => t('Native name'),
    ),
    array(
      'data' => t('Enabled'),
    ),
    array(
      'data' => t('Default'),
    ),
    array(
      'data' => t('RTL'),
    ),
    array(
      'data' => t('Translated'),
    ),
    array(
      'data' => t('Operations'),
    ),
  );
  $output = theme('table', $header, $rows);
  $output .= drupal_render($form);
  return $output;
}

/**
 * Save language settings in variable
 */
function i18n_admin_manage_screen_submit($form_id, $form_values) {

  // Save changes to existing languages.
  foreach (array(
    'site_default',
    'name',
    'rtl',
    'native',
    'enabled',
  ) as $key) {
    $save[$key] = $form_values[$key];
  }
  $save['name']['en'] = 'English';
  $languages = locale_supported_languages(FALSE, TRUE);
  foreach ($languages['name'] as $key => $lang) {

    // Nothing
  }
  variable_set('i18n_languages', $save);
}

/**
 * Check for multilingual variables in form
 */
function i18n_form_alter_settings(&$form, &$variables) {
  $result = 0;
  foreach (element_children($form) as $field) {
    if ($form[$field]['#type'] == 'fieldset') {
      $result += i18n_form_alter_settings($form[$field], $variables);
    }
    elseif (in_array($field, $variables)) {
      $form[$field]['#description'] .= ' <strong>' . t('This is a multilingual variable.') . '</strong>';
      $result++;
    }
  }
  return $result;
}

/**
 * Save multilingual variables and remove them from form
 */
function i18n_variable_form_submit($form_id, $form_values) {
  $op = isset($form_values['op']) ? $form_values['op'] : '';
  $variables = variable_get('i18n_variables', array());
  $language = i18n_get_lang();
  foreach ($form_values as $key => $value) {
    if (in_array($key, $variables)) {
      if ($op == t('Reset to defaults')) {
        i18n_variable_del($key, $language);
      }
      else {
        if (is_array($value) && isset($form_values['array_filter'])) {
          $value = array_keys(array_filter($value));
        }
        i18n_variable_set($key, $value, $language);
      }
      unset($form_values[$key]);
    }
  }

  // Re-submit form
  system_settings_form_submit($form_id, $form_values);
}

/**
 * Initialization of multilingual variables
 * 
 * @param $language
 *   Language to retrieve variables. Defaults to current language
 * @param $prefix
 *   Variable name prefix to load just a selected group of variables
 */
function i18n_variable_init($language = NULL, $prefix = '') {
  global $conf;
  global $i18n_conf;
  $language = $language ? $language : _i18n_get_lang();
  if ($i18n_variables = variable_get('i18n_variables', '')) {
    if (!$i18n_conf) {
      $i18n_conf = array();
    }
    $variables = _i18n_variable_init($language, $prefix);
    foreach ($i18n_variables as $name) {
      $i18n_conf[$name] = isset($variables[$name]) ? $variables[$name] : (isset($conf[$name]) ? $conf[$name] : '');
    }
    $conf = array_merge($conf, $i18n_conf);
  }
}

/**
 * Set a persistent language dependent variable.
 *
 * @param $name
 *   The name of the variable to set.
 * @param $value
 *   The value to set. This can be any PHP data type; these functions take care
 *   of serialization as necessary.
 */
function i18n_variable_set($name, $value, $language) {
  global $conf, $i18n_conf;
  db_lock_table('i18n_variable');
  db_query("DELETE FROM {i18n_variable} WHERE name = '%s' AND language='%s'", $name, $language);
  db_query("INSERT INTO {i18n_variable} (name, language, value) VALUES ('%s', '%s', '%s')", $name, $language, serialize($value));
  db_unlock_tables();
  cache_clear_all('variables:' . $language, 'cache');
  $conf[$name] = $value;
  $i18n_conf[$name] = $value;
}

/**
 * Unset a persistent multilingual variable.
 *
 * @param $name
 *   The name of the variable to undefine.
 */
function i18n_variable_del($name, $language) {
  global $conf, $i18n_conf;
  db_query("DELETE FROM {i18n_variable} WHERE name = '%s' AND language='%s'", $name, $language);
  cache_clear_all('variables:' . $language, 'cache');
  unset($conf[$name]);
  unset($i18n_conf[$name]);
}

/**
 * Helper function to create language selector
 */
function _i18n_language_select($value = '', $description = '', $weight = -20, $languages = NULL) {
  $languages = $languages ? $languages : i18n_language_list();
  return array(
    '#type' => 'select',
    '#title' => t('Language'),
    '#default_value' => $value,
    '#options' => array_merge(array(
      '' => '',
    ), $languages),
    '#description' => $description,
    '#weight' => $weight,
  );
}

/**
 * Load language variables into array
 */
function _i18n_variable_init($language, $prefix = '') {
  $variables = array();
  $cacheid = 'variables:' . $language . ($prefix ? ':' . $prefix : '');
  if ($cached = cache_get($cacheid)) {
    $variables = unserialize($cached->data);
  }
  else {
    $result = db_query("SELECT * FROM {i18n_variable} WHERE language='%s' AND name LIKE '%s%'", $language, $prefix);
    while ($variable = db_fetch_object($result)) {
      $variables[$variable->name] = unserialize($variable->value);
    }
    cache_set($cacheid, 'cache', serialize($variables));
  }
  return $variables;
}

/**
 * Save multilingual variables that may have been changed by other methods than settings pages
 */
function _i18n_variable_exit() {
  global $i18n_conf;
  global $conf;
  if ($i18n_conf) {
    $lang = _i18n_get_lang();
    $refresh = FALSE;

    // Rewritten because array_diff_assoc may fail with array variables
    foreach ($i18n_conf as $name => $value) {
      if ($value != $conf[$name]) {
        $refresh = TRUE;
        $i18n_conf[$name] = $conf[$name];
        db_query("DELETE FROM {i18n_variable} WHERE name='%s' AND language='%s'", $name, $lang);
        db_query("INSERT INTO {i18n_variable} (language, name, value) VALUES('%s', '%s', '%s')", $lang, $name, serialize($conf[$name]));
      }
    }
    if ($refresh) {
      cache_set('variables:' . $lang, 'cache', serialize($i18n_conf));
    }
  }
}

/**
 * Check whether we are in bootstrap mode
 */
function _i18n_is_bootstrap() {
  return !function_exists('drupal_get_headers');
}

/**
 * Sets db_prefix to given language
 */
function _i18n_set_db_prefix($lang) {
  global $db_prefix, $db_prefix_i18n;
  if (is_array($db_prefix_i18n)) {
    $db_prefix = array_merge($db_prefix, str_replace('**', $lang, $db_prefix_i18n));
  }
}

/**
 * To get the original path. 
 * Cannot use $_GET["q"] cause it may have been already changed
 */
function _i18n_get_original_path() {
  return isset($_REQUEST["q"]) ? trim($_REQUEST["q"], "/") : '';
}

/**
 * String handling functions (Object localization)
 * 
 * These are a set of wrapper functions that will call i18nstrings if enabled 
 * or use plain Drupal core's localization othewise.
 * 
 * The differences with the default localization system:
 * - It will translate strings not from English but from the default language
 * - They're not plain strings, have some context information too
 */

/**
 * Translate configurable string, and store for l10n client. 
 * 
 * This is just a wrapper for backwards compatibility. For this function to do something useful
 * you need to enable i18nstrings module, otherwise it's just a slightly improved t() for i18n. It works
 * the same but forgets about English and translates when language != default and language == locale 
 * 
 * @param $strid
 *   Textgroup and location glued with ':'
 *   I.e. profile:
 * @param $default
 *   String in default language. Default language may or may not be English
 * @param $langcode
 *   Optional language code if different from current request language
 * @param $update
 *   Whether to update/create the string
 */
function tt($strid, $default, $language = NULL, $update = FALSE) {
  global $locale;
  $language = $language ? $language : $locale;
  if (function_exists('i18nstrings_tt')) {
    return i18nstrings_tt($strid, $default, $language, $update);
  }
  elseif ($language == i18n_default_language() || $language != $locale) {

    // We just translate from default language, not to default language
    return $default;
  }
  else {

    // We cannot call t() here because it won't work for English when not default language
    return locale($default);
  }
}

Functions

Namesort descending Description
i18n_admin_manage_screen_submit Save language settings in variable
i18n_admin_settings Form builder function.
i18n_block Language block
i18n_db_rewrite_sql Implementation of hook_db_rewrite_sql()
i18n_db_rewrite_where Rewrites queries depending on rewriting mode
i18n_default_language Returns default language code
i18n_exit Implementation of hook_exit
i18n_form_alter Implementation of hook_form_alter
i18n_form_alter_settings Check for multilingual variables in form
i18n_frontpage Language dependent front page This function will search for aliases like 'en/home', 'es/home'...
i18n_get_browser_lang Get language from browser settings, but only if it is a valid language
i18n_get_lang_prefix Get language code from path.
i18n_get_links Function i18n_get_links
i18n_get_main_lang Returns main language, two letter code
i18n_get_normal_path This function is similar to drupal_get_normal_path, but language-aware Also removes language from path
i18n_help Implementation of hook_help().
i18n_init Implementation of hook_init()
i18n_languages Get locale languages plus i18n language settings
i18n_language_list Get localized language list, sort alphabetically
i18n_language_property Get language properties
i18n_language_rtl Check whether language is RTL
i18n_menu Implementation of hook_menu(). Modify rewriting conditions when viewing specific nodes
i18n_menu_edit_item_form_submit Process menu and menu item add/edit form submissions.
i18n_nodeapi Implementation of hook_nodeapi().
i18n_node_get_lang Get node language
i18n_node_language_list Get allowed languages for node
i18n_path Produces i18n paths, with language prefix If path is empty or site frontpage, path = 'lang' Check for frontpage and search for alias before adding language
i18n_perm Implementation of hook_perm().
i18n_selection_mode i18n_selection_mode
i18n_supported_languages Get list of supported languages
i18n_taxonomy Implementation of hook_taxonomy
i18n_token_list Implementation of hook_token_list().
i18n_token_values Implementation of hook_token_values().
i18n_url_rewrite Rewrites path with current language and removes prefix if searching for source path
i18n_user Implementation of hook_user()
i18n_variable_del Unset a persistent multilingual variable.
i18n_variable_form_submit Save multilingual variables and remove them from form
i18n_variable_init Initialization of multilingual variables
i18n_variable_set Set a persistent language dependent variable.
theme_i18n_admin_manage_screen Theme the locale admin manager form.
theme_i18n_language_icon Theme language icon
theme_i18n_link Produces a language link with the right flag
tt Translate configurable string, and store for l10n client.
_i18n_content_languages
_i18n_get_original_path To get the original path. Cannot use $_GET["q"] cause it may have been already changed
_i18n_goto Emulates drupal_goto, it may not be loaded yet
_i18n_is_bootstrap Check whether we are in bootstrap mode
_i18n_language_select Helper function to create language selector
_i18n_locale_supported_languages Returns list of enabled languages from locale module
_i18n_selection_mode
_i18n_set_db_prefix Sets db_prefix to given language
_i18n_variable_exit Save multilingual variables that may have been changed by other methods than settings pages
_i18n_variable_init Load language variables into array

Constants