Drupal PHP Cross Reference Content Management Systems

Source: /includes/locale.inc - 2442 lines - 84054 bytes - Summary - Text - Print

   1  <?php
   2  
   3  /**
   4   * @file
   5   * Administration functions for locale.module.
   6   */
   7  
   8  /**
   9   * The language is determined using a URL language indicator:
  10   * path prefix or domain according to the configuration.
  11   */
  12  define('LOCALE_LANGUAGE_NEGOTIATION_URL', 'locale-url');
  13  
  14  /**
  15   * The language is set based on the browser language settings.
  16   */
  17  define('LOCALE_LANGUAGE_NEGOTIATION_BROWSER', 'locale-browser');
  18  
  19  /**
  20   * The language is determined using the current interface language.
  21   */
  22  define('LOCALE_LANGUAGE_NEGOTIATION_INTERFACE', 'locale-interface');
  23  
  24  /**
  25   * If no URL language is available language is determined using an already
  26   * detected one.
  27   */
  28  define('LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK', 'locale-url-fallback');
  29  
  30  /**
  31   * The language is set based on the user language settings.
  32   */
  33  define('LOCALE_LANGUAGE_NEGOTIATION_USER', 'locale-user');
  34  
  35  /**
  36   * The language is set based on the request/session parameters.
  37   */
  38  define('LOCALE_LANGUAGE_NEGOTIATION_SESSION', 'locale-session');
  39  
  40  /**
  41   * Regular expression pattern used to localize JavaScript strings.
  42   */
  43  define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+');
  44  
  45  /**
  46   * Regular expression pattern used to match simple JS object literal.
  47   *
  48   * This pattern matches a basic JS object, but will fail on an object with
  49   * nested objects. Used in JS file parsing for string arg processing.
  50   */
  51  define('LOCALE_JS_OBJECT', '\{.*?\}');
  52  
  53  /**
  54   * Regular expression to match an object containing a key 'context'.
  55   *
  56   * Pattern to match a JS object containing a 'context key' with a string value,
  57   * which is captured. Will fail if there are nested objects.
  58   */
  59  define('LOCALE_JS_OBJECT_CONTEXT', '
  60    \{              # match object literal start
  61    .*?             # match anything, non-greedy
  62    (?:             # match a form of "context"
  63      \'context\'
  64      |
  65      "context"
  66      |
  67      context
  68    )
  69    \s*:\s*         # match key-value separator ":"
  70    (' . LOCALE_JS_STRING . ')  # match context string
  71    .*?             # match anything, non-greedy
  72    \}              # match end of object literal
  73  ');
  74  
  75  /**
  76   * Translation import mode overwriting all existing translations
  77   * if new translated version available.
  78   */
  79  define('LOCALE_IMPORT_OVERWRITE', 0);
  80  
  81  /**
  82   * Translation import mode keeping existing translations and only
  83   * inserting new strings.
  84   */
  85  define('LOCALE_IMPORT_KEEP', 1);
  86  
  87  /**
  88   * URL language negotiation: use the path prefix as URL language
  89   * indicator.
  90   */
  91  define('LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX', 0);
  92  
  93  /**
  94   * URL language negotiation: use the domain as URL language
  95   * indicator.
  96   */
  97  define('LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN', 1);
  98  
  99  /**
 100   * @defgroup locale-languages-negotiation Language negotiation options
 101   * @{
 102   * Functions for language negotiation.
 103   *
 104   * There are functions that provide the ability to identify the
 105   * language. This behavior can be controlled by various options.
 106   */
 107  
 108  /**
 109   * Identifies the language from the current interface language.
 110   *
 111   * @return
 112   *   The current interface language code.
 113   */
 114  function locale_language_from_interface() {
 115    global $language;
 116    return isset($language->language) ? $language->language : FALSE;
 117  }
 118  
 119  /**
 120   * Identify language from the Accept-language HTTP header we got.
 121   *
 122   * We perform browser accept-language parsing only if page cache is disabled,
 123   * otherwise we would cache a user-specific preference.
 124   *
 125   * @param $languages
 126   *   An array of language objects for enabled languages ordered by weight.
 127   *
 128   * @return
 129   *   A valid language code on success, FALSE otherwise.
 130   */
 131  function locale_language_from_browser($languages) {
 132    if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
 133      return FALSE;
 134    }
 135  
 136    // The Accept-Language header contains information about the language
 137    // preferences configured in the user's browser / operating system.
 138    // RFC 2616 (section 14.4) defines the Accept-Language header as follows:
 139    //   Accept-Language = "Accept-Language" ":"
 140    //                  1#( language-range [ ";" "q" "=" qvalue ] )
 141    //   language-range  = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
 142    // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
 143    $browser_langcodes = array();
 144    if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
 145      foreach ($matches as $match) {
 146        // We can safely use strtolower() here, tags are ASCII.
 147        // RFC2616 mandates that the decimal part is no more than three digits,
 148        // so we multiply the qvalue by 1000 to avoid floating point comparisons.
 149        $langcode = strtolower($match[1]);
 150        $qvalue = isset($match[2]) ? (float) $match[2] : 1;
 151        $browser_langcodes[$langcode] = (int) ($qvalue * 1000);
 152      }
 153    }
 154  
 155    // We should take pristine values from the HTTP headers, but Internet Explorer
 156    // from version 7 sends only specific language tags (eg. fr-CA) without the
 157    // corresponding generic tag (fr) unless explicitly configured. In that case,
 158    // we assume that the lowest value of the specific tags is the value of the
 159    // generic language to be as close to the HTTP 1.1 spec as possible.
 160    // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 and
 161    // http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
 162    asort($browser_langcodes);
 163    foreach ($browser_langcodes as $langcode => $qvalue) {
 164      $generic_tag = strtok($langcode, '-');
 165      if (!isset($browser_langcodes[$generic_tag])) {
 166        $browser_langcodes[$generic_tag] = $qvalue;
 167      }
 168    }
 169  
 170    // Find the enabled language with the greatest qvalue, following the rules
 171    // of RFC 2616 (section 14.4). If several languages have the same qvalue,
 172    // prefer the one with the greatest weight.
 173    $best_match_langcode = FALSE;
 174    $max_qvalue = 0;
 175    foreach ($languages as $langcode => $language) {
 176      // Language tags are case insensitive (RFC2616, sec 3.10).
 177      $langcode = strtolower($langcode);
 178  
 179      // If nothing matches below, the default qvalue is the one of the wildcard
 180      // language, if set, or is 0 (which will never match).
 181      $qvalue = isset($browser_langcodes['*']) ? $browser_langcodes['*'] : 0;
 182  
 183      // Find the longest possible prefix of the browser-supplied language
 184      // ('the language-range') that matches this site language ('the language tag').
 185      $prefix = $langcode;
 186      do {
 187        if (isset($browser_langcodes[$prefix])) {
 188          $qvalue = $browser_langcodes[$prefix];
 189          break;
 190        }
 191      }
 192      while ($prefix = substr($prefix, 0, strrpos($prefix, '-')));
 193  
 194      // Find the best match.
 195      if ($qvalue > $max_qvalue) {
 196        $best_match_langcode = $language->language;
 197        $max_qvalue = $qvalue;
 198      }
 199    }
 200  
 201    return $best_match_langcode;
 202  }
 203  
 204  /**
 205   * Identify language from the user preferences.
 206   *
 207   * @param $languages
 208   *   An array of valid language objects.
 209   *
 210   * @return
 211   *   A valid language code on success, FALSE otherwise.
 212   */
 213  function locale_language_from_user($languages) {
 214    // User preference (only for logged users).
 215    global $user;
 216  
 217    if ($user->uid) {
 218      return $user->language;
 219    }
 220  
 221    // No language preference from the user.
 222    return FALSE;
 223  }
 224  
 225  /**
 226   * Identify language from a request/session parameter.
 227   *
 228   * @param $languages
 229   *   An array of valid language objects.
 230   *
 231   * @return
 232   *   A valid language code on success, FALSE otherwise.
 233   */
 234  function locale_language_from_session($languages) {
 235    $param = variable_get('locale_language_negotiation_session_param', 'language');
 236  
 237    // Request parameter: we need to update the session parameter only if we have
 238    // an authenticated user.
 239    if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) {
 240      global $user;
 241      if ($user->uid) {
 242        $_SESSION[$param] = $langcode;
 243      }
 244      return $langcode;
 245    }
 246  
 247    // Session parameter.
 248    if (isset($_SESSION[$param])) {
 249      return $_SESSION[$param];
 250    }
 251  
 252    return FALSE;
 253  }
 254  
 255  /**
 256   * Identify language via URL prefix or domain.
 257   *
 258   * @param $languages
 259   *   An array of valid language objects.
 260   *
 261   * @return
 262   *   A valid language code on success, FALSE otherwise.
 263   */
 264  function locale_language_from_url($languages) {
 265    $language_url = FALSE;
 266  
 267    if (!language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) {
 268      return $language_url;
 269    }
 270  
 271    switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
 272      case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
 273        // $_GET['q'] might not be available at this time, because
 274        // path initialization runs after the language bootstrap phase.
 275        list($language, $_GET['q']) = language_url_split_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages);
 276        if ($language !== FALSE) {
 277          $language_url = $language->language;
 278        }
 279        break;
 280  
 281      case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
 282        // Get only the host, not the port.
 283        $http_host= $_SERVER['HTTP_HOST'];
 284        if (strpos($http_host, ':') !== FALSE) {
 285          $http_host_tmp = explode(':', $http_host);
 286          $http_host = current($http_host_tmp);
 287        }
 288        foreach ($languages as $language) {
 289          // Skip check if the language doesn't have a domain.
 290          if ($language->domain) {
 291            // Only compare the domains not the protocols or ports.
 292            // Remove protocol and add http:// so parse_url works
 293            $host = 'http://' . str_replace(array('http://', 'https://'), '', $language->domain);
 294            $host = parse_url($host, PHP_URL_HOST);
 295            if ($http_host == $host) {
 296              $language_url = $language->language;
 297              break;
 298            }
 299          }
 300        }
 301        break;
 302    }
 303  
 304    return $language_url;
 305  }
 306  
 307  /**
 308   * Determines the language to be assigned to URLs when none is detected.
 309   *
 310   * The language negotiation process has a fallback chain that ends with the
 311   * default language provider. Each built-in language type has a separate
 312   * initialization:
 313   * - Interface language, which is the only configurable one, always gets a valid
 314   *   value. If no request-specific language is detected, the default language
 315   *   will be used.
 316   * - Content language merely inherits the interface language by default.
 317   * - URL language is detected from the requested URL and will be used to rewrite
 318   *   URLs appearing in the page being rendered. If no language can be detected,
 319   *   there are two possibilities:
 320   *   - If the default language has no configured path prefix or domain, then the
 321   *     default language is used. This guarantees that (missing) URL prefixes are
 322   *     preserved when navigating through the site.
 323   *   - If the default language has a configured path prefix or domain, a
 324   *     requested URL having an empty prefix or domain is an anomaly that must be
 325   *     fixed. This is done by introducing a prefix or domain in the rendered
 326   *     page matching the detected interface language.
 327   *
 328   * @param $languages
 329   *   (optional) An array of valid language objects. This is passed by
 330   *   language_provider_invoke() to every language provider callback, but it is
 331   *   not actually needed here. Defaults to NULL.
 332   * @param $language_type
 333   *   (optional) The language type to fall back to. Defaults to the interface
 334   *   language.
 335   *
 336   * @return
 337   *   A valid language code.
 338   */
 339  function locale_language_url_fallback($language = NULL, $language_type = LANGUAGE_TYPE_INTERFACE) {
 340    $default = language_default();
 341    $prefix = (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX);
 342  
 343    // If the default language is not configured to convey language information,
 344    // a missing URL language information indicates that URL language should be
 345    // the default one, otherwise we fall back to an already detected language.
 346    if (($prefix && empty($default->prefix)) || (!$prefix && empty($default->domain))) {
 347      return $default->language;
 348    }
 349    else {
 350      return $GLOBALS[$language_type]->language;
 351    }
 352  }
 353  
 354  /**
 355   * Return the URL language switcher block. Translation links may be provided by
 356   * other modules.
 357   */
 358  function locale_language_switcher_url($type, $path) {
 359    $languages = language_list('enabled');
 360    $links = array();
 361  
 362    foreach ($languages[1] as $language) {
 363      $links[$language->language] = array(
 364        'href'       => $path,
 365        'title'      => $language->native,
 366        'language'   => $language,
 367        'attributes' => array('class' => array('language-link')),
 368      );
 369    }
 370  
 371    return $links;
 372  }
 373  
 374  /**
 375   * Return the session language switcher block.
 376   */
 377  function locale_language_switcher_session($type, $path) {
 378    drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
 379  
 380    $param = variable_get('locale_language_negotiation_session_param', 'language');
 381    $language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $GLOBALS[$type]->language;
 382  
 383    $languages = language_list('enabled');
 384    $links = array();
 385  
 386    $query = $_GET;
 387    unset($query['q']);
 388  
 389    foreach ($languages[1] as $language) {
 390      $langcode = $language->language;
 391      $links[$langcode] = array(
 392        'href'       => $path,
 393        'title'      => $language->native,
 394        'attributes' => array('class' => array('language-link')),
 395        'query'      => $query,
 396      );
 397      if ($language_query != $langcode) {
 398        $links[$langcode]['query'][$param] = $langcode;
 399      }
 400      else {
 401        $links[$langcode]['attributes']['class'][] = ' session-active';
 402      }
 403    }
 404  
 405    return $links;
 406  }
 407  
 408  /**
 409   * Rewrite URLs for the URL language provider.
 410   */
 411  function locale_language_url_rewrite_url(&$path, &$options) {
 412    static $drupal_static_fast;
 413    if (!isset($drupal_static_fast)) {
 414      $drupal_static_fast['languages'] = &drupal_static(__FUNCTION__);
 415    }
 416    $languages = &$drupal_static_fast['languages'];
 417  
 418    if (!isset($languages)) {
 419      $languages = language_list('enabled');
 420      $languages = array_flip(array_keys($languages[1]));
 421    }
 422  
 423    // Language can be passed as an option, or we go for current URL language.
 424    if (!isset($options['language'])) {
 425      global $language_url;
 426      $options['language'] = $language_url;
 427    }
 428    // We allow only enabled languages here.
 429    elseif (!isset($languages[$options['language']->language])) {
 430      unset($options['language']);
 431      return;
 432    }
 433  
 434    if (isset($options['language'])) {
 435      switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
 436        case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
 437          if ($options['language']->domain) {
 438            // Ask for an absolute URL with our modified base_url.
 439            global $is_https;
 440            $url_scheme = ($is_https) ? 'https://' : 'http://';
 441            $options['absolute'] = TRUE;
 442  
 443            // Take the domain without ports or protocols so we can apply the
 444            // protocol needed. The setting might include a protocol.
 445            // This is changed in Drupal 8 but we need to keep backwards
 446            // compatibility for Drupal 7.
 447            $host = 'http://' . str_replace(array('http://', 'https://'), '', $options['language']->domain);
 448            $host = parse_url($host, PHP_URL_HOST);
 449  
 450            // Apply the appropriate protocol to the URL.
 451            $options['base_url'] = $url_scheme . $host;
 452            if (isset($options['https']) && variable_get('https', FALSE)) {
 453              if ($options['https'] === TRUE) {
 454                $options['base_url'] = str_replace('http://', 'https://', $options['base_url']);
 455              }
 456              elseif ($options['https'] === FALSE) {
 457                $options['base_url'] = str_replace('https://', 'http://', $options['base_url']);
 458              }
 459            }
 460          }
 461          break;
 462  
 463        case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
 464          if (!empty($options['language']->prefix)) {
 465            $options['prefix'] = $options['language']->prefix . '/';
 466          }
 467          break;
 468      }
 469    }
 470  }
 471  
 472  /**
 473   * Rewrite URLs for the Session language provider.
 474   */
 475  function locale_language_url_rewrite_session(&$path, &$options) {
 476    static $query_rewrite, $query_param, $query_value;
 477  
 478    // The following values are not supposed to change during a single page
 479    // request processing.
 480    if (!isset($query_rewrite)) {
 481      global $user;
 482      if (!$user->uid) {
 483        $languages = language_list('enabled');
 484        $languages = $languages[1];
 485        $query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language'));
 486        $query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL;
 487        $query_rewrite = isset($languages[$query_value]) && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION);
 488      }
 489      else {
 490        $query_rewrite = FALSE;
 491      }
 492    }
 493  
 494    // If the user is anonymous, the user language provider is enabled, and the
 495    // corresponding option has been set, we must preserve any explicit user
 496    // language preference even with cookies disabled.
 497    if ($query_rewrite) {
 498      if (is_string($options['query'])) {
 499        $options['query'] = drupal_get_query_array($options['query']);
 500      }
 501      if (!isset($options['query'][$query_param])) {
 502        $options['query'][$query_param] = $query_value;
 503      }
 504    }
 505  }
 506  
 507  /**
 508   * @} End of "locale-languages-negotiation"
 509   */
 510  
 511  /**
 512   * Check that a string is safe to be added or imported as a translation.
 513   *
 514   * This test can be used to detect possibly bad translation strings. It should
 515   * not have any false positives. But it is only a test, not a transformation,
 516   * as it destroys valid HTML. We cannot reliably filter translation strings
 517   * on import because some strings are irreversibly corrupted. For example,
 518   * a &amp; in the translation would get encoded to &amp;amp; by filter_xss()
 519   * before being put in the database, and thus would be displayed incorrectly.
 520   *
 521   * The allowed tag list is like filter_xss_admin(), but omitting div and img as
 522   * not needed for translation and likely to cause layout issues (div) or a
 523   * possible attack vector (img).
 524   */
 525  function locale_string_is_safe($string) {
 526    return decode_entities($string) == decode_entities(filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var')));
 527  }
 528  
 529  /**
 530   * @defgroup locale-api-add Language addition API
 531   * @{
 532   * Add a language.
 533   *
 534   * The language addition API is used to create languages and store them.
 535   */
 536  
 537  /**
 538   * API function to add a language.
 539   *
 540   * @param $langcode
 541   *   Language code.
 542   * @param $name
 543   *   English name of the language
 544   * @param $native
 545   *   Native name of the language
 546   * @param $direction
 547   *   LANGUAGE_LTR or LANGUAGE_RTL
 548   * @param $domain
 549   *   Optional custom domain name with protocol, without
 550   *   trailing slash (eg. http://de.example.com).
 551   * @param $prefix
 552   *   Optional path prefix for the language. Defaults to the
 553   *   language code if omitted.
 554   * @param $enabled
 555   *   Optionally TRUE to enable the language when created or FALSE to disable.
 556   * @param $default
 557   *   Optionally set this language to be the default.
 558   */
 559  function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) {
 560    // Default prefix on language code.
 561    if (empty($prefix)) {
 562      $prefix = $langcode;
 563    }
 564  
 565    // If name was not set, we add a predefined language.
 566    if (!isset($name)) {
 567      include_once  DRUPAL_ROOT . '/includes/iso.inc';
 568      $predefined = _locale_get_predefined_list();
 569      $name = $predefined[$langcode][0];
 570      $native = isset($predefined[$langcode][1]) ? $predefined[$langcode][1] : $predefined[$langcode][0];
 571      $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR;
 572    }
 573  
 574    db_insert('languages')
 575      ->fields(array(
 576        'language' => $langcode,
 577        'name' => $name,
 578        'native' => $native,
 579        'direction' => $direction,
 580        'domain' => $domain,
 581        'prefix' => $prefix,
 582        'enabled' => $enabled,
 583      ))
 584      ->execute();
 585  
 586    // Only set it as default if enabled.
 587    if ($enabled && $default) {
 588      variable_set('language_default', (object) array('language' => $langcode, 'name' => $name, 'native' => $native, 'direction' => $direction, 'enabled' => (int) $enabled, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => $prefix, 'weight' => 0, 'javascript' => ''));
 589    }
 590  
 591    if ($enabled) {
 592      // Increment enabled language count if we are adding an enabled language.
 593      variable_set('language_count', variable_get('language_count', 1) + 1);
 594    }
 595  
 596    // Kill the static cache in language_list().
 597    drupal_static_reset('language_list');
 598  
 599    // Force JavaScript translation file creation for the newly added language.
 600    _locale_invalidate_js($langcode);
 601  
 602    watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode));
 603  
 604    module_invoke_all('multilingual_settings_changed');
 605  }
 606  /**
 607   * @} End of "locale-api-add"
 608   */
 609  
 610  /**
 611   * @defgroup locale-api-import-export Translation import/export API.
 612   * @{
 613   * Functions to import and export translations.
 614   *
 615   * These functions provide the ability to import translations from
 616   * external files and to export translations and translation templates.
 617   */
 618  
 619  /**
 620   * Parses Gettext Portable Object file information and inserts into database
 621   *
 622   * @param $file
 623   *   Drupal file object corresponding to the PO file to import.
 624   * @param $langcode
 625   *   Language code.
 626   * @param $mode
 627   *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
 628   *   LOCALE_IMPORT_OVERWRITE.
 629   * @param $group
 630   *   Text group to import PO file into (eg. 'default' for interface
 631   *   translations).
 632   */
 633  function _locale_import_po($file, $langcode, $mode, $group = NULL) {
 634    // Try to allocate enough time to parse and import the data.
 635    drupal_set_time_limit(240);
 636  
 637    // Check if we have the language already in the database.
 638    if (!db_query("SELECT COUNT(language) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) {
 639      drupal_set_message(t('The language selected for import is not supported.'), 'error');
 640      return FALSE;
 641    }
 642  
 643    // Get strings from file (returns on failure after a partial import, or on success)
 644    $status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group);
 645    if ($status === FALSE) {
 646      // Error messages are set in _locale_import_read_po().
 647      return FALSE;
 648    }
 649  
 650    // Get status information on import process.
 651    list($header_done, $additions, $updates, $deletes, $skips) = _locale_import_one_string('db-report');
 652  
 653    if (!$header_done) {
 654      drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error');
 655    }
 656  
 657    // Clear cache and force refresh of JavaScript translations.
 658    _locale_invalidate_js($langcode);
 659    cache_clear_all('locale:', 'cache', TRUE);
 660  
 661    // Rebuild the menu, strings may have changed.
 662    menu_rebuild();
 663  
 664    drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)));
 665    watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes));
 666    if ($skips) {
 667      $skip_message = format_plural($skips, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.');
 668      drupal_set_message($skip_message);
 669      watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING);
 670    }
 671    return TRUE;
 672  }
 673  
 674  /**
 675   * Parses Gettext Portable Object file into an array
 676   *
 677   * @param $op
 678   *   Storage operation type: db-store or mem-store.
 679   * @param $file
 680   *   Drupal file object corresponding to the PO file to import.
 681   * @param $mode
 682   *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
 683   *   LOCALE_IMPORT_OVERWRITE.
 684   * @param $lang
 685   *   Language code.
 686   * @param $group
 687   *   Text group to import PO file into (eg. 'default' for interface
 688   *   translations).
 689   */
 690  function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
 691  
 692    // The file will get closed by PHP on returning from this function.
 693    $fd = fopen($file->uri, 'rb');
 694    if (!$fd) {
 695      _locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
 696      return FALSE;
 697    }
 698  
 699    /*
 700     * The parser context. Can be:
 701     *  - 'COMMENT' (#)
 702     *  - 'MSGID' (msgid)
 703     *  - 'MSGID_PLURAL' (msgid_plural)
 704     *  - 'MSGCTXT' (msgctxt)
 705     *  - 'MSGSTR' (msgstr or msgstr[])
 706     *  - 'MSGSTR_ARR' (msgstr_arg)
 707     */
 708    $context = 'COMMENT';
 709  
 710    // Current entry being read.
 711    $current = array();
 712  
 713    // Current plurality for 'msgstr[]'.
 714    $plural = 0;
 715  
 716    // Current line.
 717    $lineno = 0;
 718  
 719    while (!feof($fd)) {
 720      // A line should not be longer than 10 * 1024.
 721      $line = fgets($fd, 10 * 1024);
 722  
 723      if ($lineno == 0) {
 724        // The first line might come with a UTF-8 BOM, which should be removed.
 725        $line = str_replace("\xEF\xBB\xBF", '', $line);
 726      }
 727  
 728      $lineno++;
 729  
 730      // Trim away the linefeed.
 731      $line = trim(strtr($line, array("\\\n" => "")));
 732  
 733      if (!strncmp('#', $line, 1)) {
 734        // Lines starting with '#' are comments.
 735  
 736        if ($context == 'COMMENT') {
 737          // Already in comment token, insert the comment.
 738          $current['#'][] = substr($line, 1);
 739        }
 740        elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
 741          // We are currently in string token, close it out.
 742          _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
 743  
 744          // Start a new entry for the comment.
 745          $current         = array();
 746          $current['#'][]  = substr($line, 1);
 747  
 748          $context = 'COMMENT';
 749        }
 750        else {
 751          // A comment following any other token is a syntax error.
 752          _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
 753          return FALSE;
 754        }
 755      }
 756      elseif (!strncmp('msgid_plural', $line, 12)) {
 757        // A plural form for the current message.
 758  
 759        if ($context != 'MSGID') {
 760          // A plural form cannot be added to anything else but the id directly.
 761          _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
 762          return FALSE;
 763        }
 764  
 765        // Remove 'msgid_plural' and trim away whitespace.
 766        $line = trim(substr($line, 12));
 767        // At this point, $line should now contain only the plural form.
 768  
 769        $quoted = _locale_import_parse_quoted($line);
 770        if ($quoted === FALSE) {
 771          // The plural form must be wrapped in quotes.
 772          _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
 773          return FALSE;
 774        }
 775  
 776        // Append the plural form to the current entry.
 777        $current['msgid'] .= "\0" . $quoted;
 778  
 779        $context = 'MSGID_PLURAL';
 780      }
 781      elseif (!strncmp('msgid', $line, 5)) {
 782        // Starting a new message.
 783  
 784        if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
 785          // We are currently in a message string, close it out.
 786          _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
 787  
 788          // Start a new context for the id.
 789          $current = array();
 790        }
 791        elseif ($context == 'MSGID') {
 792          // We are currently already in the context, meaning we passed an id with no data.
 793          _locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno);
 794          return FALSE;
 795        }
 796  
 797        // Remove 'msgid' and trim away whitespace.
 798        $line = trim(substr($line, 5));
 799        // At this point, $line should now contain only the message id.
 800  
 801        $quoted = _locale_import_parse_quoted($line);
 802        if ($quoted === FALSE) {
 803          // The message id must be wrapped in quotes.
 804          _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
 805          return FALSE;
 806        }
 807  
 808        $current['msgid'] = $quoted;
 809        $context = 'MSGID';
 810      }
 811      elseif (!strncmp('msgctxt', $line, 7)) {
 812        // Starting a new context.
 813  
 814        if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
 815          // We are currently in a message, start a new one.
 816          _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
 817          $current = array();
 818        }
 819        elseif (!empty($current['msgctxt'])) {
 820          // A context cannot apply to another context.
 821          _locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno);
 822          return FALSE;
 823        }
 824  
 825        // Remove 'msgctxt' and trim away whitespaces.
 826        $line = trim(substr($line, 7));
 827        // At this point, $line should now contain the context.
 828  
 829        $quoted = _locale_import_parse_quoted($line);
 830        if ($quoted === FALSE) {
 831          // The context string must be quoted.
 832          _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
 833          return FALSE;
 834        }
 835  
 836        $current['msgctxt'] = $quoted;
 837  
 838        $context = 'MSGCTXT';
 839      }
 840      elseif (!strncmp('msgstr[', $line, 7)) {
 841        // A message string for a specific plurality.
 842  
 843        if (($context != 'MSGID') && ($context != 'MSGCTXT') && ($context != 'MSGID_PLURAL') && ($context != 'MSGSTR_ARR')) {
 844          // Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries.
 845          _locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno);
 846          return FALSE;
 847        }
 848  
 849        // Ensure the plurality is terminated.
 850        if (strpos($line, ']') === FALSE) {
 851          _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
 852          return FALSE;
 853        }
 854  
 855        // Extract the plurality.
 856        $frombracket = strstr($line, '[');
 857        $plural = substr($frombracket, 1, strpos($frombracket, ']') - 1);
 858  
 859        // Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data.
 860        $line = trim(strstr($line, " "));
 861  
 862        $quoted = _locale_import_parse_quoted($line);
 863        if ($quoted === FALSE) {
 864          // The string must be quoted.
 865          _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
 866          return FALSE;
 867        }
 868  
 869        $current['msgstr'][$plural] = $quoted;
 870  
 871        $context = 'MSGSTR_ARR';
 872      }
 873      elseif (!strncmp("msgstr", $line, 6)) {
 874        // A string for the an id or context.
 875  
 876        if (($context != 'MSGID') && ($context != 'MSGCTXT')) {
 877          // Strings are only valid within an id or context scope.
 878          _locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno);
 879          return FALSE;
 880        }
 881  
 882        // Remove 'msgstr' and trim away away whitespaces.
 883        $line = trim(substr($line, 6));
 884        // At this point, $line should now contain the message.
 885  
 886        $quoted = _locale_import_parse_quoted($line);
 887        if ($quoted === FALSE) {
 888          // The string must be quoted.
 889          _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
 890          return FALSE;
 891        }
 892  
 893        $current['msgstr'] = $quoted;
 894  
 895        $context = 'MSGSTR';
 896      }
 897      elseif ($line != '') {
 898        // Anything that is not a token may be a continuation of a previous token.
 899  
 900        $quoted = _locale_import_parse_quoted($line);
 901        if ($quoted === FALSE) {
 902          // The string must be quoted.
 903          _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
 904          return FALSE;
 905        }
 906  
 907        // Append the string to the current context.
 908        if (($context == 'MSGID') || ($context == 'MSGID_PLURAL')) {
 909          $current['msgid'] .= $quoted;
 910        }
 911        elseif ($context == 'MSGCTXT') {
 912          $current['msgctxt'] .= $quoted;
 913        }
 914        elseif ($context == 'MSGSTR') {
 915          $current['msgstr'] .= $quoted;
 916        }
 917        elseif ($context == 'MSGSTR_ARR') {
 918          $current['msgstr'][$plural] .= $quoted;
 919        }
 920        else {
 921          // No valid context to append to.
 922          _locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno);
 923          return FALSE;
 924        }
 925      }
 926    }
 927  
 928    // End of PO file, closed out the last entry.
 929    if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
 930      _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
 931    }
 932    elseif ($context != 'COMMENT') {
 933      _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
 934      return FALSE;
 935    }
 936  }
 937  
 938  /**
 939   * Sets an error message occurred during locale file parsing.
 940   *
 941   * @param $message
 942   *   The message to be translated.
 943   * @param $file
 944   *   Drupal file object corresponding to the PO file to import.
 945   * @param $lineno
 946   *   An optional line number argument.
 947   */
 948  function _locale_import_message($message, $file, $lineno = NULL) {
 949    $vars = array('%filename' => $file->filename);
 950    if (isset($lineno)) {
 951      $vars['%line'] = $lineno;
 952    }
 953    $t = get_t();
 954    drupal_set_message($t($message, $vars), 'error');
 955  }
 956  
 957  /**
 958   * Imports a string into the database
 959   *
 960   * @param $op
 961   *   Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'.
 962   * @param $value
 963   *   Details of the string stored.
 964   * @param $mode
 965   *   Should existing translations be replaced LOCALE_IMPORT_KEEP or
 966   *   LOCALE_IMPORT_OVERWRITE.
 967   * @param $lang
 968   *   Language to store the string in.
 969   * @param $file
 970   *   Object representation of file being imported, only required when op is
 971   *   'db-store'.
 972   * @param $group
 973   *   Text group to import PO file into (eg. 'default' for interface
 974   *   translations).
 975   */
 976  function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') {
 977    $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
 978    $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
 979    $strings = &drupal_static(__FUNCTION__ . ':strings', array());
 980  
 981    switch ($op) {
 982      // Return stored strings
 983      case 'mem-report':
 984        return $strings;
 985  
 986      // Store string in memory (only supports single strings)
 987      case 'mem-store':
 988        $strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr'];
 989        return;
 990  
 991      // Called at end of import to inform the user
 992      case 'db-report':
 993        return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']);
 994  
 995      // Store the string we got in the database.
 996      case 'db-store':
 997        // We got header information.
 998        if ($value['msgid'] == '') {
 999          $languages = language_list();
1000          if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) {
1001            // Since we only need to parse the header if we ought to update the
1002            // plural formula, only run this if we don't need to keep existing
1003            // data untouched or if we don't have an existing plural formula.
1004            $header = _locale_import_parse_header($value['msgstr']);
1005  
1006            // Get and store the plural formula if available.
1007            if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) {
1008              list($nplurals, $plural) = $p;
1009              db_update('languages')
1010                ->fields(array(
1011                  'plurals' => $nplurals,
1012                  'formula' => $plural,
1013                ))
1014                ->condition('language', $lang)
1015                ->execute();
1016            }
1017          }
1018          $header_done = TRUE;
1019        }
1020  
1021        else {
1022          // Some real string to import.
1023          $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']);
1024  
1025          if (strpos($value['msgid'], "\0")) {
1026            // This string has plural versions.
1027            $english = explode("\0", $value['msgid'], 2);
1028            $entries = array_keys($value['msgstr']);
1029            for ($i = 3; $i <= count($entries); $i++) {
1030              $english[] = $english[1];
1031            }
1032            $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries);
1033            $english = array_map('_locale_import_append_plural', $english, $entries);
1034            foreach ($translation as $key => $trans) {
1035              if ($key == 0) {
1036                $plid = 0;
1037              }
1038              $plid = _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $group, $comments, $mode, $plid, $key);
1039            }
1040          }
1041  
1042          else {
1043            // A simple string to import.
1044            $english = $value['msgid'];
1045            $translation = $value['msgstr'];
1046            _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $group, $comments, $mode);
1047          }
1048        }
1049    } // end of db-store operation
1050  }
1051  
1052  /**
1053   * Import one string into the database.
1054   *
1055   * @param $report
1056   *   Report array summarizing the number of changes done in the form:
1057   *   array(inserts, updates, deletes).
1058   * @param $langcode
1059   *   Language code to import string into.
1060   * @param $context
1061   *   The context of this string.
1062   * @param $source
1063   *   Source string.
1064   * @param $translation
1065   *   Translation to language specified in $langcode.
1066   * @param $textgroup
1067   *   Name of textgroup to store translation in.
1068   * @param $location
1069   *   Location value to save with source string.
1070   * @param $mode
1071   *   Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
1072   * @param $plid
1073   *   Optional plural ID to use.
1074   * @param $plural
1075   *   Optional plural value to use.
1076   *
1077   * @return
1078   *   The string ID of the existing string modified or the new string added.
1079   */
1080  function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $plid = 0, $plural = 0) {
1081    $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $source, ':context' => $context, ':textgroup' => $textgroup))->fetchField();
1082  
1083    if (!empty($translation)) {
1084      // Skip this string unless it passes a check for dangerous code.
1085      // Text groups other than default still can contain HTML tags
1086      // (i.e. translatable blocks).
1087      if ($textgroup == "default" && !locale_string_is_safe($translation)) {
1088        $report['skips']++;
1089        $lid = 0;
1090      }
1091      elseif ($lid) {
1092        // We have this source string saved already.
1093        db_update('locales_source')
1094          ->fields(array(
1095            'location' => $location,
1096          ))
1097          ->condition('lid', $lid)
1098          ->execute();
1099  
1100        $exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField();
1101  
1102        if (!$exists) {
1103          // No translation in this language.
1104          db_insert('locales_target')
1105            ->fields(array(
1106              'lid' => $lid,
1107              'language' => $langcode,
1108              'translation' => $translation,
1109              'plid' => $plid,
1110              'plural' => $plural,
1111            ))
1112            ->execute();
1113  
1114          $report['additions']++;
1115        }
1116        elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
1117          // Translation exists, only overwrite if instructed.
1118          db_update('locales_target')
1119            ->fields(array(
1120              'translation' => $translation,
1121              'plid' => $plid,
1122              'plural' => $plural,
1123            ))
1124            ->condition('language', $langcode)
1125            ->condition('lid', $lid)
1126            ->execute();
1127  
1128          $report['updates']++;
1129        }
1130      }
1131      else {
1132        // No such source string in the database yet.
1133        $lid = db_insert('locales_source')
1134          ->fields(array(
1135            'location' => $location,
1136            'source' => $source,
1137            'context' => (string) $context,
1138            'textgroup' => $textgroup,
1139          ))
1140          ->execute();
1141  
1142        db_insert('locales_target')
1143          ->fields(array(
1144             'lid' => $lid,
1145             'language' => $langcode,
1146             'translation' => $translation,
1147             'plid' => $plid,
1148             'plural' => $plural
1149          ))
1150          ->execute();
1151  
1152        $report['additions']++;
1153      }
1154    }
1155    elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
1156      // Empty translation, remove existing if instructed.
1157      db_delete('locales_target')
1158        ->condition('language', $langcode)
1159        ->condition('lid', $lid)
1160        ->condition('plid', $plid)
1161        ->condition('plural', $plural)
1162        ->execute();
1163  
1164      $report['deletes']++;
1165    }
1166  
1167    return $lid;
1168  }
1169  
1170  /**
1171   * Parses a Gettext Portable Object file header
1172   *
1173   * @param $header
1174   *   A string containing the complete header.
1175   *
1176   * @return
1177   *   An associative array of key-value pairs.
1178   */
1179  function _locale_import_parse_header($header) {
1180    $header_parsed = array();
1181    $lines = array_map('trim', explode("\n", $header));
1182    foreach ($lines as $line) {
1183      if ($line) {
1184        list($tag, $contents) = explode(":", $line, 2);
1185        $header_parsed[trim($tag)] = trim($contents);
1186      }
1187    }
1188    return $header_parsed;
1189  }
1190  
1191  /**
1192   * Parses a Plural-Forms entry from a Gettext Portable Object file header
1193   *
1194   * @param $pluralforms
1195   *   A string containing the Plural-Forms entry.
1196   * @param $filepath
1197   *   A string containing the filepath.
1198   *
1199   * @return
1200   *   An array containing the number of plurals and a
1201   *   formula in PHP for computing the plural form.
1202   */
1203  function _locale_import_parse_plural_forms($pluralforms, $filepath) {
1204    // First, delete all whitespace
1205    $pluralforms = strtr($pluralforms, array(" " => "", "\t" => ""));
1206  
1207    // Select the parts that define nplurals and plural
1208    $nplurals = strstr($pluralforms, "nplurals=");
1209    if (strpos($nplurals, ";")) {
1210      $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9);
1211    }
1212    else {
1213      return FALSE;
1214    }
1215    $plural = strstr($pluralforms, "plural=");
1216    if (strpos($plural, ";")) {
1217      $plural = substr($plural, 7, strpos($plural, ";") - 7);
1218    }
1219    else {
1220      return FALSE;
1221    }
1222  
1223    // Get PHP version of the plural formula
1224    $plural = _locale_import_parse_arithmetic($plural);
1225  
1226    if ($plural !== FALSE) {
1227      return array($nplurals, $plural);
1228    }
1229    else {
1230      drupal_set_message(t('The translation file %filepath contains an error: the plural formula could not be parsed.', array('%filepath' => $filepath)), 'error');
1231      return FALSE;
1232    }
1233  }
1234  
1235  /**
1236   * Parses and sanitizes an arithmetic formula into a PHP expression
1237   *
1238   * While parsing, we ensure, that the operators have the right
1239   * precedence and associativity.
1240   *
1241   * @param $string
1242   *   A string containing the arithmetic formula.
1243   *
1244   * @return
1245   *   The PHP version of the formula.
1246   */
1247  function _locale_import_parse_arithmetic($string) {
1248    // Operator precedence table
1249    $precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8);
1250    // Right associativity
1251    $right_associativity = array("?" => 1, ":" => 1);
1252  
1253    $tokens = _locale_import_tokenize_formula($string);
1254  
1255    // Parse by converting into infix notation then back into postfix
1256    // Operator stack - holds math operators and symbols
1257    $operator_stack = array();
1258    // Element Stack - holds data to be operated on
1259    $element_stack = array();
1260  
1261    foreach ($tokens as $token) {
1262      $current_token = $token;
1263  
1264      // Numbers and the $n variable are simply pushed into $element_stack
1265      if (is_numeric($token)) {
1266        $element_stack[] = $current_token;
1267      }
1268      elseif ($current_token == "n") {
1269        $element_stack[] = '$n';
1270      }
1271      elseif ($current_token == "(") {
1272        $operator_stack[] = $current_token;
1273      }
1274      elseif ($current_token == ")") {
1275        $topop = array_pop($operator_stack);
1276        while (isset($topop) && ($topop != "(")) {
1277          $element_stack[] = $topop;
1278          $topop = array_pop($operator_stack);
1279        }
1280      }
1281      elseif (!empty($precedence[$current_token])) {
1282        // If it's an operator, then pop from $operator_stack into $element_stack until the
1283        // precedence in $operator_stack is less than current, then push into $operator_stack
1284        $topop = array_pop($operator_stack);
1285        while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) {
1286          $element_stack[] = $topop;
1287          $topop = array_pop($operator_stack);
1288        }
1289        if ($topop) {
1290          $operator_stack[] = $topop;   // Return element to top
1291        }
1292        $operator_stack[] = $current_token;      // Parentheses are not needed
1293      }
1294      else {
1295        return FALSE;
1296      }
1297    }
1298  
1299    // Flush operator stack
1300    $topop = array_pop($operator_stack);
1301    while ($topop != NULL) {
1302      $element_stack[] = $topop;
1303      $topop = array_pop($operator_stack);
1304    }
1305  
1306    // Now extract formula from stack
1307    $previous_size = count($element_stack) + 1;
1308    while (count($element_stack) < $previous_size) {
1309      $previous_size = count($element_stack);
1310      for ($i = 2; $i < count($element_stack); $i++) {
1311        $op = $element_stack[$i];
1312        if (!empty($precedence[$op])) {
1313          $f = "";
1314          if ($op == ":") {
1315            $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")";
1316          }
1317          elseif ($op == "?") {
1318            $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1];
1319          }
1320          else {
1321            $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")";
1322          }
1323          array_splice($element_stack, $i - 2, 3, $f);
1324          break;
1325        }
1326      }
1327    }
1328  
1329    // If only one element is left, the number of operators is appropriate
1330    if (count($element_stack) == 1) {
1331      return $element_stack[0];
1332    }
1333    else {
1334      return FALSE;
1335    }
1336  }
1337  
1338  /**
1339   * Backward compatible implementation of token_get_all() for formula parsing
1340   *
1341   * @param $string
1342   *   A string containing the arithmetic formula.
1343   *
1344   * @return
1345   *   The PHP version of the formula.
1346   */
1347  function _locale_import_tokenize_formula($formula) {
1348    $formula = str_replace(" ", "", $formula);
1349    $tokens = array();
1350    for ($i = 0; $i < strlen($formula); $i++) {
1351      if (is_numeric($formula[$i])) {
1352        $num = $formula[$i];
1353        $j = $i + 1;
1354        while ($j < strlen($formula) && is_numeric($formula[$j])) {
1355          $num .= $formula[$j];
1356          $j++;
1357        }
1358        $i = $j - 1;
1359        $tokens[] = $num;
1360      }
1361      elseif ($pos = strpos(" =<>!&|", $formula[$i])) { // We won't have a space
1362        $next = $formula[$i + 1];
1363        switch ($pos) {
1364          case 1:
1365          case 2:
1366          case 3:
1367          case 4:
1368            if ($next == '=') {
1369              $tokens[] = $formula[$i] . '=';
1370              $i++;
1371            }
1372            else {
1373              $tokens[] = $formula[$i];
1374            }
1375            break;
1376          case 5:
1377            if ($next == '&') {
1378              $tokens[] = '&&';
1379              $i++;
1380            }
1381            else {
1382              $tokens[] = $formula[$i];
1383            }
1384            break;
1385          case 6:
1386            if ($next == '|') {
1387              $tokens[] = '||';
1388              $i++;
1389            }
1390            else {
1391              $tokens[] = $formula[$i];
1392            }
1393            break;
1394        }
1395      }
1396      else {
1397        $tokens[] = $formula[$i];
1398      }
1399    }
1400    return $tokens;
1401  }
1402  
1403  /**
1404   * Modify a string to contain proper count indices
1405   *
1406   * This is a callback function used via array_map()
1407   *
1408   * @param $entry
1409   *   An array element.
1410   * @param $key
1411   *   Index of the array element.
1412   */
1413  function _locale_import_append_plural($entry, $key) {
1414    // No modifications for 0, 1
1415    if ($key == 0 || $key == 1) {
1416      return $entry;
1417    }
1418  
1419    // First remove any possibly false indices, then add new ones
1420    $entry = preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry);
1421    return preg_replace('/(@count)/', "\\1[$key]", $entry);
1422  }
1423  
1424  /**
1425   * Generate a short, one string version of the passed comment array
1426   *
1427   * @param $comment
1428   *   An array of strings containing a comment.
1429   *
1430   * @return
1431   *   Short one string version of the comment.
1432   */
1433  function _locale_import_shorten_comments($comment) {
1434    $comm = '';
1435    while (count($comment)) {
1436      $test = $comm . substr(array_shift($comment), 1) . ', ';
1437      if (strlen($comm) < 130) {
1438        $comm = $test;
1439      }
1440      else {
1441        break;
1442      }
1443    }
1444    return trim(substr($comm, 0, -2));
1445  }
1446  
1447  /**
1448   * Parses a string in quotes
1449   *
1450   * @param $string
1451   *   A string specified with enclosing quotes.
1452   *
1453   * @return
1454   *   The string parsed from inside the quotes.
1455   */
1456  function _locale_import_parse_quoted($string) {
1457    if (substr($string, 0, 1) != substr($string, -1, 1)) {
1458      return FALSE;   // Start and end quotes must be the same
1459    }
1460    $quote = substr($string, 0, 1);
1461    $string = substr($string, 1, -1);
1462    if ($quote == '"') {        // Double quotes: strip slashes
1463      return stripcslashes($string);
1464    }
1465    elseif ($quote == "'") {  // Simple quote: return as-is
1466      return $string;
1467    }
1468    else {
1469      return FALSE;             // Unrecognized quote
1470    }
1471  }
1472  /**
1473   * @} End of "locale-api-import-export"
1474   */
1475  
1476  /**
1477   * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and
1478   * Drupal.formatPlural() and inserts them into the database.
1479   */
1480  function _locale_parse_js_file($filepath) {
1481    global $language;
1482  
1483    // The file path might contain a query string, so make sure we only use the
1484    // actual file.
1485    $parsed_url = drupal_parse_url($filepath);
1486    $filepath = $parsed_url['path'];
1487    // Load the JavaScript file.
1488    $file = file_get_contents($filepath);
1489  
1490    // Match all calls to Drupal.t() in an array.
1491    // Note: \s also matches newlines with the 's' modifier.
1492    preg_match_all('~
1493      [^\w]Drupal\s*\.\s*t\s*                       # match "Drupal.t" with whitespace
1494      \(\s*                                         # match "(" argument list start
1495      (' . LOCALE_JS_STRING . ')\s*                 # capture string argument
1496      (?:,\s*' . LOCALE_JS_OBJECT . '\s*            # optionally capture str args
1497        (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*) # optionally capture context
1498      ?)?                                           # close optional args
1499      [,\)]                                         # match ")" or "," to finish
1500      ~sx', $file, $t_matches);
1501  
1502    // Match all Drupal.formatPlural() calls in another array.
1503    preg_match_all('~
1504      [^\w]Drupal\s*\.\s*formatPlural\s*  # match "Drupal.formatPlural" with whitespace
1505      \(                                  # match "(" argument list start
1506      \s*.+?\s*,\s*                       # match count argument
1507      (' . LOCALE_JS_STRING . ')\s*,\s*   # match singular string argument
1508      (                             # capture plural string argument
1509        (?:                         # non-capturing group to repeat string pieces
1510          (?:
1511            \'                      # match start of single-quoted string
1512            (?:\\\\\'|[^\'])*       # match any character except unescaped single-quote
1513            @count                  # match "@count"
1514            (?:\\\\\'|[^\'])*       # match any character except unescaped single-quote
1515            \'                      # match end of single-quoted string
1516            |
1517            "                       # match start of double-quoted string
1518            (?:\\\\"|[^"])*         # match any character except unescaped double-quote
1519            @count                  # match "@count"
1520            (?:\\\\"|[^"])*         # match any character except unescaped double-quote
1521            "                       # match end of double-quoted string
1522          )
1523          (?:\s*\+\s*)?             # match "+" with possible whitespace, for str concat
1524        )+                          # match multiple because we supports concatenating strs
1525      )\s*                          # end capturing of plural string argument
1526      (?:,\s*' . LOCALE_JS_OBJECT . '\s*          # optionally capture string args
1527        (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*)?  # optionally capture context
1528      )?
1529      [,\)]
1530      ~sx', $file, $plural_matches);
1531  
1532    $matches = array();
1533  
1534    // Add strings from Drupal.t().
1535    foreach ($t_matches[1] as $key => $string) {
1536      $matches[] = array(
1537        'string'  => $string,
1538        'context' => $t_matches[2][$key],
1539      );
1540    }
1541  
1542    // Add string from Drupal.formatPlural().
1543    foreach ($plural_matches[1] as $key => $string) {
1544      $matches[] = array(
1545        'string'  => $string,
1546        'context' => $plural_matches[3][$key],
1547      );
1548  
1549      // If there is also a plural version of this string, add it to the strings array.
1550      if (isset($plural_matches[2][$key])) {
1551        $matches[] = array(
1552          'string'  => $plural_matches[2][$key],
1553          'context' => $plural_matches[3][$key],
1554        );
1555      }
1556    }
1557  
1558    foreach ($matches as $key => $match) {
1559      // Remove the quotes and string concatenations from the string.
1560      $string = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['string'], 1, -1)));
1561      $context = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['context'], 1, -1)));
1562  
1563      $source = db_query("SELECT lid, location FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = 'default'", array(':source' => $string, ':context' => $context))->fetchObject();
1564      if ($source) {
1565        // We already have this source string and now have to add the location
1566        // to the location column, if this file is not yet present in there.
1567        $locations = preg_split('~\s*;\s*~', $source->location);
1568  
1569        if (!in_array($filepath, $locations)) {
1570          $locations[] = $filepath;
1571          $locations = implode('; ', $locations);
1572  
1573          // Save the new locations string to the database.
1574          db_update('locales_source')
1575            ->fields(array(
1576              'location' => $locations,
1577            ))
1578            ->condition('lid', $source->lid)
1579            ->execute();
1580        }
1581      }
1582      else {
1583        // We don't have the source string yet, thus we insert it into the database.
1584        db_insert('locales_source')
1585          ->fields(array(
1586            'location' => $filepath,
1587            'source' => $string,
1588            'context' => $context,
1589            'textgroup' => 'default',
1590          ))
1591          ->execute();
1592      }
1593    }
1594  }
1595  
1596  /**
1597   * @addtogroup locale-api-import-export
1598   * @{
1599   */
1600  
1601  /**
1602   * Generates a structured array of all strings with translations in
1603   * $language, if given. This array can be used to generate an export
1604   * of the string in the database.
1605   *
1606   * @param $language
1607   *   Language object to generate the output for, or NULL if generating
1608   *   translation template.
1609   * @param $group
1610   *   Text group to export PO file from (eg. 'default' for interface
1611   *   translations).
1612   */
1613  function _locale_export_get_strings($language = NULL, $group = 'default') {
1614    if (isset($language)) {
1615      $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.textgroup = :textgroup ORDER BY t.plid, t.plural", array(':language' => $language->language, ':textgroup' => $group));
1616    }
1617    else {
1618      $result = db_query("SELECT s.lid, s.source, s.context, s.location, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.textgroup = :textgroup ORDER BY t.plid, t.plural", array(':textgroup' => $group));
1619    }
1620    $strings = array();
1621    foreach ($result as $child) {
1622      $string = array(
1623        'comment'     => $child->location,
1624        'source'      => $child->source,
1625        'context'     => $child->context,
1626        'translation' => isset($child->translation) ? $child->translation : '',
1627      );
1628      if ($child->plid) {
1629        // Has a parent lid. Since we process in the order of plids,
1630        // we already have the parent in the array, so we can add the
1631        // lid to the next plural version to it. This builds a linked
1632        // list of plurals.
1633        $string['child'] = TRUE;
1634        $strings[$child->plid]['plural'] = $child->lid;
1635      }
1636      $strings[$child->lid] = $string;
1637    }
1638    return $strings;
1639  }
1640  
1641  /**
1642   * Generates the PO(T) file contents for given strings.
1643   *
1644   * @param $language
1645   *   Language object to generate the output for, or NULL if generating
1646   *   translation template.
1647   * @param $strings
1648   *   Array of strings to export. See _locale_export_get_strings()
1649   *   on how it should be formatted.
1650   * @param $header
1651   *   The header portion to use for the output file. Defaults
1652   *   are provided for PO and POT files.
1653   */
1654  function _locale_export_po_generate($language = NULL, $strings = array(), $header = NULL) {
1655    global $user;
1656  
1657    if (!isset($header)) {
1658      if (isset($language)) {
1659        $header = '# ' . $language->name . ' translation of ' . variable_get('site_name', 'Drupal') . "\n";
1660        $header .= '# Generated by ' . $user->name . ' <' . $user->mail . ">\n";
1661        $header .= "#\n";
1662        $header .= "msgid \"\"\n";
1663        $header .= "msgstr \"\"\n";
1664        $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
1665        $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
1666        $header .= "\"PO-Revision-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
1667        $header .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
1668        $header .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
1669        $header .= "\"MIME-Version: 1.0\\n\"\n";
1670        $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
1671        $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
1672        if ($language->formula && $language->plurals) {
1673          $header .= "\"Plural-Forms: nplurals=" . $language->plurals . "; plural=" . strtr($language->formula, array('$' => '')) . ";\\n\"\n";
1674        }
1675      }
1676      else {
1677        $header = "# LANGUAGE translation of PROJECT\n";
1678        $header .= "# Copyright (c) YEAR NAME <EMAIL@ADDRESS>\n";
1679        $header .= "#\n";
1680        $header .= "msgid \"\"\n";
1681        $header .= "msgstr \"\"\n";
1682        $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
1683        $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n";
1684        $header .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n";
1685        $header .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
1686        $header .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
1687        $header .= "\"MIME-Version: 1.0\\n\"\n";
1688        $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
1689        $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
1690        $header .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n";
1691      }
1692    }
1693  
1694    $output = $header . "\n";
1695  
1696    foreach ($strings as $lid => $string) {
1697      // Only process non-children, children are output below their parent.
1698      if (!isset($string['child'])) {
1699        if ($string['comment']) {
1700          $output .= '#: ' . $string['comment'] . "\n";
1701        }
1702        if (!empty($string['context'])) {
1703          $output .= 'msgctxt ' . _locale_export_string($string['context']);
1704        }
1705        $output .= 'msgid ' . _locale_export_string($string['source']);
1706        if (!empty($string['plural'])) {
1707          $plural = $string['plural'];
1708          $output .= 'msgid_plural ' . _locale_export_string($strings[$plural]['source']);
1709          if (isset($language)) {
1710            $translation = $string['translation'];
1711            for ($i = 0; $i < $language->plurals; $i++) {
1712              $output .= 'msgstr[' . $i . '] ' . _locale_export_string($translation);
1713              if ($plural) {
1714                $translation = _locale_export_remove_plural($strings[$plural]['translation']);
1715                $plural = isset($strings[$plural]['plural']) ? $strings[$plural]['plural'] : 0;
1716              }
1717              else {
1718                $translation = '';
1719              }
1720            }
1721          }
1722          else {
1723            $output .= 'msgstr[0] ""' . "\n";
1724            $output .= 'msgstr[1] ""' . "\n";
1725          }
1726        }
1727        else {
1728          $output .= 'msgstr ' . _locale_export_string($string['translation']);
1729        }
1730        $output .= "\n";
1731      }
1732    }
1733    return $output;
1734  }
1735  
1736  /**
1737   * Write a generated PO or POT file to the output.
1738   *
1739   * @param $language
1740   *   Language object to generate the output for, or NULL if generating
1741   *   translation template.
1742   * @param $output
1743   *   The PO(T) file to output as a string. See _locale_export_generate_po()
1744   *   on how it can be generated.
1745   */
1746  function _locale_export_po($language = NULL, $output = NULL) {
1747    // Log the export event.
1748    if (isset($language)) {
1749      $filename = $language->language . '.po';
1750      watchdog('locale', 'Exported %locale translation file: %filename.', array('%locale' => $language->name, '%filename' => $filename));
1751    }
1752    else {
1753      $filename = 'drupal.pot';
1754      watchdog('locale', 'Exported translation file: %filename.', array('%filename' => $filename));
1755    }
1756    // Download the file for the client.
1757    header("Content-Disposition: attachment; filename=$filename");
1758    header("Content-Type: text/plain; charset=utf-8");
1759    print $output;
1760    drupal_exit();
1761  }
1762  
1763  /**
1764   * Print out a string on multiple lines
1765   */
1766  function _locale_export_string($str) {
1767    $stri = addcslashes($str, "\0..\37\\\"");
1768    $parts = array();
1769  
1770    // Cut text into several lines
1771    while ($stri != "") {
1772      $i = strpos($stri, "\\n");
1773      if ($i === FALSE) {
1774        $curstr = $stri;
1775        $stri = "";
1776      }
1777      else {
1778        $curstr = substr($stri, 0, $i + 2);
1779        $stri = substr($stri, $i + 2);
1780      }
1781      $curparts = explode("\n", _locale_export_wrap($curstr, 70));
1782      $parts = array_merge($parts, $curparts);
1783    }
1784  
1785    // Multiline string
1786    if (count($parts) > 1) {
1787      return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n";
1788    }
1789    // Single line string
1790    elseif (count($parts) == 1) {
1791      return "\"$parts[0]\"\n";
1792    }
1793    // No translation
1794    else {
1795      return "\"\"\n";
1796    }
1797  }
1798  
1799  /**
1800   * Custom word wrapping for Portable Object (Template) files.
1801   */
1802  function _locale_export_wrap($str, $len) {
1803    $words = explode(' ', $str);
1804    $return = array();
1805  
1806    $cur = "";
1807    $nstr = 1;
1808    while (count($words)) {
1809      $word = array_shift($words);
1810      if ($nstr) {
1811        $cur = $word;
1812        $nstr = 0;
1813      }
1814      elseif (strlen("$cur $word") > $len) {
1815        $return[] = $cur . " ";
1816        $cur = $word;
1817      }
1818      else {
1819        $cur = "$cur $word";
1820      }
1821    }
1822    $return[] = $cur;
1823  
1824    return implode("\n", $return);
1825  }
1826  
1827  /**
1828   * Removes plural index information from a string
1829   */
1830  function _locale_export_remove_plural($entry) {
1831    return preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry);
1832  }
1833  /**
1834   * @} End of "locale-api-import-export"
1835   */
1836  
1837  /**
1838   * @defgroup locale-api-seek Translation search API
1839   * @{
1840   * Functions to search in translation files.
1841   *
1842   * These functions provide the functionality to search for specific
1843   * translations.
1844   */
1845  
1846  /**
1847   * Perform a string search and display results in a table
1848   */
1849  function _locale_translate_seek() {
1850    $output = '';
1851  
1852    // We have at least one criterion to match
1853    if (!($query = _locale_translate_seek_query())) {
1854      $query = array(
1855        'translation' => 'all',
1856        'group' => 'all',
1857        'language' => 'all',
1858        'string' => '',
1859      );
1860    }
1861  
1862    $sql_query = db_select('locales_source', 's');
1863  
1864    $limit_language = NULL;
1865    if ($query['language'] != 'en' && $query['language'] != 'all') {
1866      $sql_query->leftJoin('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array(':langcode' => $query['language']));
1867      $limit_language = $query['language'];
1868    }
1869    else {
1870      $sql_query->leftJoin('locales_target', 't', 't.lid = s.lid');
1871    }
1872  
1873    $sql_query->fields('s', array('source', 'location', 'context', 'lid', 'textgroup'));
1874    $sql_query->fields('t', array('translation', 'language'));
1875  
1876    // Compute LIKE section.
1877    switch ($query['translation']) {
1878      case 'translated':
1879        $sql_query->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE');
1880        $sql_query->orderBy('t.translation', 'DESC');
1881        break;
1882      case 'untranslated':
1883        $sql_query->condition(db_and()
1884          ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE')
1885          ->isNull('t.translation')
1886        );
1887        $sql_query->orderBy('s.source');
1888        break;
1889      case 'all' :
1890      default:
1891        $condition = db_or()
1892          ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE');
1893        if ($query['language'] != 'en') {
1894          // Only search in translations if the language is not forced to English.
1895          $condition->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE');
1896        }
1897        $sql_query->condition($condition);
1898        break;
1899    }
1900  
1901    // Add a condition on the text group.
1902    if (!empty($query['group']) && $query['group'] != 'all') {
1903      $sql_query->condition('s.textgroup', $query['group']);
1904    }
1905  
1906    $sql_query = $sql_query->extend('PagerDefault')->limit(50);
1907    $locales = $sql_query->execute();
1908  
1909    $groups = module_invoke_all('locale', 'groups');
1910    $header = array(t('Text group'), t('String'), t('Context'), ($limit_language) ? t('Language') : t('Languages'), array('data' => t('Operations'), 'colspan' => '2'));
1911  
1912    $strings = array();
1913    foreach ($locales as $locale) {
1914      if (!isset($strings[$locale->lid])) {
1915        $strings[$locale->lid] = array(
1916          'group' => $locale->textgroup,
1917          'languages' => array(),
1918          'location' => $locale->location,
1919          'source' => $locale->source,
1920          'context' => $locale->context,
1921        );
1922      }
1923      if (isset($locale->language)) {
1924        $strings[$locale->lid]['languages'][$locale->language] = $locale->translation;
1925      }
1926    }
1927  
1928    $rows = array();
1929    foreach ($strings as $lid => $string) {
1930      $rows[] = array(
1931        $groups[$string['group']],
1932        array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
1933        $string['context'],
1934        array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'),
1935        array('data' => l(t('edit'), "admin/config/regional/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')),
1936        array('data' => l(t('delete'), "admin/config/regional/translate/delete/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')),
1937      );
1938    }
1939  
1940    $output .= theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('No strings available.')));
1941    $output .= theme('pager');
1942  
1943    return $output;
1944  }
1945  
1946  /**
1947   * Build array out of search criteria specified in request variables
1948   */
1949  function _locale_translate_seek_query() {
1950    $query = &drupal_static(__FUNCTION__);
1951    if (!isset($query)) {
1952      $query = array();
1953      $fields = array('string', 'language', 'translation', 'group');
1954      foreach ($fields as $field) {
1955        if (isset($_SESSION['locale_translation_filter'][$field])) {
1956          $query[$field] = $_SESSION['locale_translation_filter'][$field];
1957        }
1958      }
1959    }
1960    return $query;
1961  }
1962  
1963  /**
1964   * Force the JavaScript translation file(s) to be refreshed.
1965   *
1966   * This function sets a refresh flag for a specified language, or all
1967   * languages except English, if none specified. JavaScript translation
1968   * files are rebuilt (with locale_update_js_files()) the next time a
1969   * request is served in that language.
1970   *
1971   * @param $langcode
1972   *   The language code for which the file needs to be refreshed.
1973   *
1974   * @return
1975   *   New content of the 'javascript_parsed' variable.
1976   */
1977  function _locale_invalidate_js($langcode = NULL) {
1978    $parsed = variable_get('javascript_parsed', array());
1979  
1980    if (empty($langcode)) {
1981      // Invalidate all languages.
1982      $languages = language_list();
1983      unset($languages['en']);
1984      foreach ($languages as $lcode => $data) {
1985        $parsed['refresh:' . $lcode] = 'waiting';
1986      }
1987    }
1988    else {
1989      // Invalidate single language.
1990      $parsed['refresh:' . $langcode] = 'waiting';
1991    }
1992  
1993    variable_set('javascript_parsed', $parsed);
1994    return $parsed;
1995  }
1996  
1997  /**
1998   * (Re-)Creates the JavaScript translation file for a language.
1999   *
2000   * @param $language
2001   *   The language, the translation file should be (re)created for.
2002   */
2003  function _locale_rebuild_js($langcode = NULL) {
2004    if (!isset($langcode)) {
2005      global $language;
2006    }
2007    else {
2008      // Get information about the locale.
2009      $languages = language_list();
2010      $language = $languages[$langcode];
2011    }
2012  
2013    // Construct the array for JavaScript translations.
2014    // Only add strings with a translation to the translations array.
2015    $result = db_query("SELECT s.lid, s.source, s.context, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%' AND s.textgroup = :textgroup", array(':language' => $language->language, ':textgroup' => 'default'));
2016  
2017    $translations = array();
2018    foreach ($result as $data) {
2019      $translations[$data->context][$data->source] = $data->translation;
2020    }
2021  
2022    // Construct the JavaScript file, if there are translations.
2023    $data_hash = NULL;
2024    $data = $status = '';
2025    if (!empty($translations)) {
2026  
2027      $data = "Drupal.locale = { ";
2028  
2029      if (!empty($language->formula)) {
2030        $data .= "'pluralFormula': function (\$n) { return Number({$language->formula}); }, ";
2031      }
2032  
2033      $data .= "'strings': " . drupal_json_encode($translations) . " };";
2034      $data_hash = drupal_hash_base64($data);
2035    }
2036  
2037    // Construct the filepath where JS translation files are stored.
2038    // There is (on purpose) no front end to edit that variable.
2039    $dir = 'public://' . variable_get('locale_js_directory', 'languages');
2040  
2041    // Delete old file, if we have no translations anymore, or a different file to be saved.
2042    $changed_hash = $language->javascript != $data_hash;
2043    if (!empty($language->javascript) && (!$data || $changed_hash)) {
2044      file_unmanaged_delete($dir . '/' . $language->language . '_' . $language->javascript . '.js');
2045      $language->javascript = '';
2046      $status = 'deleted';
2047    }
2048  
2049    // Only create a new file if the content has changed or the original file got
2050    // lost.
2051    $dest = $dir . '/' . $language->language . '_' . $data_hash . '.js';
2052    if ($data && ($changed_hash || !file_exists($dest))) {
2053      // Ensure that the directory exists and is writable, if possible.
2054      file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
2055  
2056      // Save the file.
2057      if (file_unmanaged_save_data($data, $dest)) {
2058        $language->javascript = $data_hash;
2059        // If we deleted a previous version of the file and we replace it with a
2060        // new one we have an update.
2061        if ($status == 'deleted') {
2062          $status = 'updated';
2063        }
2064        // If the file did not exist previously and the data has changed we have
2065        // a fresh creation.
2066        elseif ($changed_hash) {
2067          $status = 'created';
2068        }
2069        // If the data hash is unchanged the translation was lost and has to be
2070        // rebuilt.
2071        else {
2072          $status = 'rebuilt';
2073        }
2074      }
2075      else {
2076        $language->javascript = '';
2077        $status = 'error';
2078      }
2079    }
2080  
2081    // Save the new JavaScript hash (or an empty value if the file just got
2082    // deleted). Act only if some operation was executed that changed the hash
2083    // code.
2084    if ($status && $changed_hash) {
2085      db_update('languages')
2086        ->fields(array(
2087          'javascript' => $language->javascript,
2088        ))
2089        ->condition('language', $language->language)
2090        ->execute();
2091  
2092      // Update the default language variable if the default language has been altered.
2093      // This is necessary to keep the variable consistent with the database
2094      // version of the language and to prevent checking against an outdated hash.
2095      $default_langcode = language_default('language');
2096      if ($default_langcode == $language->language) {
2097        $default = db_query("SELECT * FROM {languages} WHERE language = :language", array(':language' => $default_langcode))->fetchObject();
2098        variable_set('language_default', $default);
2099      }
2100    }
2101  
2102    // Log the operation and return success flag.
2103    switch ($status) {
2104      case 'updated':
2105        watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => t($language->name)));
2106        return TRUE;
2107      case 'rebuilt':
2108        watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $language->javascript), WATCHDOG_WARNING);
2109        // Proceed to the 'created' case as the JavaScript translation file has
2110        // been created again.
2111      case 'created':
2112        watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => t($language->name)));
2113        return TRUE;
2114      case 'deleted':
2115        watchdog('locale', 'Removed JavaScript translation file for the language %language, because no translations currently exist for that language.', array('%language' => t($language->name)));
2116        return TRUE;
2117      case 'error':
2118        watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => t($language->name)), WATCHDOG_ERROR);
2119        return FALSE;
2120      default:
2121        // No operation needed.
2122        return TRUE;
2123    }
2124  }
2125  
2126  /**
2127   * List languages in search result table
2128   */
2129  function _locale_translate_language_list($translation, $limit_language) {
2130    // Add CSS.
2131    drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
2132  
2133    $languages = language_list();
2134    unset($languages['en']);
2135    $output = '';
2136    foreach ($languages as $langcode => $language) {
2137      if (!$limit_language || $limit_language == $langcode) {
2138        $output .= (!empty($translation[$langcode])) ? $langcode . ' ' : "<em class=\"locale-untranslated\">$langcode</em> ";
2139      }
2140    }
2141  
2142    return $output;
2143  }
2144  /**
2145   * @} End of "locale-api-seek"
2146   */
2147  
2148  /**
2149   * @defgroup locale-api-predefined List of predefined languages
2150   * @{
2151   * API to provide a list of predefined languages.
2152   */
2153  
2154  /**
2155   * Prepares the language code list for a select form item with only the unsupported ones
2156   */
2157  function _locale_prepare_predefined_list() {
2158    include_once  DRUPAL_ROOT . '/includes/iso.inc';
2159    $languages = language_list();
2160    $predefined = _locale_get_predefined_list();
2161    foreach ($predefined as $key => $value) {
2162      if (isset($languages[$key])) {
2163        unset($predefined[$key]);
2164        continue;
2165      }
2166      // Include native name in output, if possible
2167      if (count($value) > 1) {
2168        $tname = t($value[0]);
2169        $predefined[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])";
2170      }
2171      else {
2172        $predefined[$key] = t($value[0]);
2173      }
2174    }
2175    asort($predefined);
2176    return $predefined;
2177  }
2178  
2179  /**
2180   * @} End of "locale-api-languages-predefined"
2181   */
2182  
2183  /**
2184   * @defgroup locale-autoimport Automatic interface translation import
2185   * @{
2186   * Functions to create batches for importing translations.
2187   *
2188   * These functions can be used to import translations for installed
2189   * modules.
2190   */
2191  
2192  /**
2193   * Prepare a batch to import translations for all enabled
2194   * modules in a given language.
2195   *
2196   * @param $langcode
2197   *   Language code to import translations for.
2198   * @param $finished
2199   *   Optional finished callback for the batch.
2200   * @param $skip
2201   *   Array of component names to skip. Used in the installer for the
2202   *   second pass import, when most components are already imported.
2203   *
2204   * @return
2205   *   A batch structure or FALSE if no files found.
2206   */
2207  function locale_batch_by_language($langcode, $finished = NULL, $skip = array()) {
2208    // Collect all files to import for all enabled modules and themes.
2209    $files = array();
2210    $components = array();
2211    $query = db_select('system', 's');
2212    $query->fields('s', array('name', 'filename'));
2213    $query->condition('s.status', 1);
2214    if (count($skip)) {
2215      $query->condition('name', $skip, 'NOT IN');
2216    }
2217    $result = $query->execute();
2218    foreach ($result as $component) {
2219      // Collect all files for all components, names as $langcode.po or
2220      // with names ending with $langcode.po. This allows for filenames
2221      // like node-module.de.po to let translators use small files and
2222      // be able to import in smaller chunks.
2223      $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)' . $langcode . '\.po$/', array('recurse' => FALSE)));
2224      $components[] = $component->name;
2225    }
2226  
2227    return _locale_batch_build($files, $finished, $components);
2228  }
2229  
2230  /**
2231   * Prepare a batch to run when installing modules or enabling themes.
2232   *
2233   * This batch will import translations for the newly added components
2234   * in all the languages already set up on the site.
2235   *
2236   * @param $components
2237   *   An array of component (theme and/or module) names to import
2238   *   translations for.
2239   * @param $finished
2240   *   Optional finished callback for the batch.
2241   */
2242  function locale_batch_by_component($components, $finished = '_locale_batch_system_finished') {
2243    $files = array();
2244    $languages = language_list('enabled');
2245    unset($languages[1]['en']);
2246    if (count($languages[1])) {
2247      $language_list = join('|', array_keys($languages[1]));
2248      // Collect all files to import for all $components.
2249      $result = db_query("SELECT name, filename FROM {system} WHERE status = 1");
2250      foreach ($result as $component) {
2251        if (in_array($component->name, $components)) {
2252          // Collect all files for this component in all enabled languages, named
2253          // as $langcode.po or with names ending with $langcode.po. This allows
2254          // for filenames like node-module.de.po to let translators use small
2255          // files and be able to import in smaller chunks.
2256          $files = array_merge($files, file_scan_directory(dirname($component->filename) . '/translations', '/(^|\.)(' . $language_list . ')\.po$/', array('recurse' => FALSE)));
2257        }
2258      }
2259      return _locale_batch_build($files, $finished);
2260    }
2261    return FALSE;
2262  }
2263  
2264  /**
2265   * Build a locale batch from an array of files.
2266   *
2267   * @param $files
2268   *   Array of files to import.
2269   * @param $finished
2270   *   Optional finished callback for the batch.
2271   * @param $components
2272   *   Optional list of component names the batch covers. Used in the installer.
2273   *
2274   * @return
2275   *   A batch structure.
2276   */
2277  function _locale_batch_build($files, $finished = NULL, $components = array()) {
2278    $t = get_t();
2279    if (count($files)) {
2280      $operations = array();
2281      foreach ($files as $file) {
2282        // We call _locale_batch_import for every batch operation.
2283        $operations[] = array('_locale_batch_import', array($file->uri));
2284      }
2285      $batch = array(
2286        'operations'    => $operations,
2287        'title'         => $t('Importing interface translations'),
2288        'init_message'  => $t('Starting import'),
2289        'error_message' => $t('Error importing interface translations'),
2290        'file'          => 'includes/locale.inc',
2291        // This is not a batch API construct, but data passed along to the
2292        // installer, so we know what did we import already.
2293        '#components'   => $components,
2294      );
2295      if (isset($finished)) {
2296        $batch['finished'] = $finished;
2297      }
2298      return $batch;
2299    }
2300    return FALSE;
2301  }
2302  
2303  /**
2304   * Perform interface translation import as a batch step.
2305   *
2306   * @param $filepath
2307   *   Path to a file to import.
2308   * @param $results
2309   *   Contains a list of files imported.
2310   */
2311  function _locale_batch_import($filepath, &$context) {
2312    // The filename is either {langcode}.po or {prefix}.{langcode}.po, so
2313    // we can extract the language code to use for the import from the end.
2314    if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) {
2315      $file = (object) array('filename' => drupal_basename($filepath), 'uri' => $filepath);
2316      _locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]);
2317      $context['results'][] = $filepath;
2318    }
2319  }
2320  
2321  /**
2322   * Finished callback of system page locale import batch.
2323   * Inform the user of translation files imported.
2324   */
2325  function _locale_batch_system_finished($success, $results) {
2326    if ($success) {
2327      drupal_set_message(format_plural(count($results), 'One translation file imported for the newly installed modules.', '@count translation files imported for the newly installed modules.'));
2328    }
2329  }
2330  
2331  /**
2332   * Finished callback of language addition locale import batch.
2333   * Inform the user of translation files imported.
2334   */
2335  function _locale_batch_language_finished($success, $results) {
2336    if ($success) {
2337      drupal_set_message(format_plural(count($results), 'One translation file imported for the enabled modules.', '@count translation files imported for the enabled modules.'));
2338    }
2339  }
2340  
2341  /**
2342   * @} End of "locale-autoimport"
2343   */
2344  
2345  /**
2346   * Get list of all predefined and custom countries.
2347   *
2348   * @return
2349   *   An array of all country code => country name pairs.
2350   */
2351  function country_get_list() {
2352    include_once  DRUPAL_ROOT . '/includes/iso.inc';
2353    $countries = _country_get_predefined_list();
2354    // Allow other modules to modify the country list.
2355    drupal_alter('countries', $countries);
2356    return $countries;
2357  }
2358  
2359  /**
2360   * Save locale specific date formats to the database.
2361   *
2362   * @param $langcode
2363   *   Language code, can be 2 characters, e.g. 'en' or 5 characters, e.g.
2364   *   'en-CA'.
2365   * @param $type
2366   *   Date format type, e.g. 'short', 'medium'.
2367   * @param $format
2368   *   The date format string.
2369   */
2370  function locale_date_format_save($langcode, $type, $format) {
2371    $locale_format = array();
2372    $locale_format['language'] = $langcode;
2373    $locale_format['type'] = $type;
2374    $locale_format['format'] = $format;
2375  
2376    $is_existing = (bool) db_query_range('SELECT 1 FROM {date_format_locale} WHERE language = :langcode AND type = :type', 0, 1, array(':langcode' => $langcode, ':type' => $type))->fetchField();
2377    if ($is_existing) {
2378      $keys = array('type', 'language');
2379      drupal_write_record('date_format_locale', $locale_format, $keys);
2380    }
2381    else {
2382      drupal_write_record('date_format_locale', $locale_format);
2383    }
2384  }
2385  
2386  /**
2387   * Select locale date format details from database.
2388   *
2389   * @param $languages
2390   *   An array of language codes.
2391   *
2392   * @return
2393   *   An array of date formats.
2394   */
2395  function locale_get_localized_date_format($languages) {
2396    $formats = array();
2397  
2398    // Get list of different format types.
2399    $format_types = system_get_date_types();
2400    $short_default = variable_get('date_format_short', 'm/d/Y - H:i');
2401  
2402    // Loop through each language until we find one with some date formats
2403    // configured.
2404    foreach ($languages as $language) {
2405      $date_formats = system_date_format_locale($language);
2406      if (!empty($date_formats)) {
2407        // We have locale-specific date formats, so check for their types. If
2408        // we're missing a type, use the default setting instead.
2409        foreach ($format_types as $type => $type_info) {
2410          // If format exists for this language, use it.
2411          if (!empty($date_formats[$type])) {
2412            $formats['date_format_' . $type] = $date_formats[$type];
2413          }
2414          // Otherwise get default variable setting. If this is not set, default
2415          // to the short format.
2416          else {
2417            $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default);
2418          }
2419        }
2420  
2421        // Return on the first match.
2422        return $formats;
2423      }
2424    }
2425  
2426    // No locale specific formats found, so use defaults.
2427    $system_types = array('short', 'medium', 'long');
2428    // Handle system types separately as they have defaults if no variable exists.
2429    $formats['date_format_short'] = $short_default;
2430    $formats['date_format_medium'] = variable_get('date_format_medium', 'D, m/d/Y - H:i');
2431    $formats['date_format_long'] = variable_get('date_format_long', 'l, F j, Y - H:i');
2432  
2433    // For non-system types, get the default setting, otherwise use the short
2434    // format.
2435    foreach ($format_types as $type => $type_info) {
2436      if (!in_array($type, $system_types)) {
2437        $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default);
2438      }
2439    }
2440  
2441    return $formats;
2442  }

title

Description

title

Description

title

Description

title

title

Body