locale.inc 32.2 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
Dries's avatar
 
Dries committed
2

Dries's avatar
 
Dries committed
3 4
/**
 * @file
5
 * Administration functions for locale.module.
Dries's avatar
 
Dries committed
6 7
 */

8 9 10 11
/**
 * The language is determined using a URL language indicator:
 * path prefix or domain according to the configuration.
 */
12
const LANGUAGE_NEGOTIATION_URL = 'locale-url';
13 14 15 16

/**
 * The language is set based on the browser language settings.
 */
17
const LANGUAGE_NEGOTIATION_BROWSER = 'locale-browser';
18 19 20 21

/**
 * The language is determined using the current interface language.
 */
22
const LANGUAGE_NEGOTIATION_INTERFACE = 'locale-interface';
23

24 25 26 27
/**
 * If no URL language is available language is determined using an already
 * detected one.
 */
28
const LANGUAGE_NEGOTIATION_URL_FALLBACK = 'locale-url-fallback';
29

30 31 32
/**
 * The language is set based on the user language settings.
 */
33
const LANGUAGE_NEGOTIATION_USER = 'locale-user';
34 35 36 37

/**
 * The language is set based on the request/session parameters.
 */
38
const LANGUAGE_NEGOTIATION_SESSION = 'locale-session';
39

40 41 42
/**
 * Regular expression pattern used to localize JavaScript strings.
 */
43
const LOCALE_JS_STRING = '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+';
44

45 46 47 48 49 50
/**
 * Regular expression pattern used to match simple JS object literal.
 *
 * This pattern matches a basic JS object, but will fail on an object with
 * nested objects. Used in JS file parsing for string arg processing.
 */
51
const LOCALE_JS_OBJECT = '\{.*?\}';
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74

/**
 * Regular expression to match an object containing a key 'context'.
 *
 * Pattern to match a JS object containing a 'context key' with a string value,
 * which is captured. Will fail if there are nested objects.
 */
define('LOCALE_JS_OBJECT_CONTEXT', '
  \{              # match object literal start
  .*?             # match anything, non-greedy
  (?:             # match a form of "context"
    \'context\'
    |
    "context"
    |
    context
  )
  \s*:\s*         # match key-value separator ":"
  (' . LOCALE_JS_STRING . ')  # match context string
  .*?             # match anything, non-greedy
  \}              # match end of object literal
');

75 76 77 78
/**
 * Translation import mode overwriting all existing translations
 * if new translated version available.
 */
79
const LOCALE_IMPORT_OVERWRITE = 0;
80 81 82 83 84

/**
 * Translation import mode keeping existing translations and only
 * inserting new strings.
 */
85
const LOCALE_IMPORT_KEEP = 1;
86

87 88 89 90
/**
 * URL language negotiation: use the path prefix as URL language
 * indicator.
 */
91
const LANGUAGE_NEGOTIATION_URL_PREFIX = 0;
92 93 94 95 96

/**
 * URL language negotiation: use the domain as URL language
 * indicator.
 */
97
const LANGUAGE_NEGOTIATION_URL_DOMAIN = 1;
98

99
/**
100
 * @defgroup locale-languages-negotiation Language negotiation options
101
 * @{
102 103 104 105
 * Functions for language negotiation.
 *
 * There are functions that provide the ability to identify the
 * 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 116
  global $language_interface;
  return isset($language_interface->langcode) ? $language_interface->langcode : FALSE;
117 118 119 120 121 122 123 124 125
}

/**
 * Identify language from the Accept-language HTTP header we got.
 *
 * We perform browser accept-language parsing only if page cache is disabled,
 * otherwise we would cache a user-specific preference.
 *
 * @param $languages
126
 *   An array of language objects for enabled languages ordered by weight.
127 128 129 130 131
 *
 * @return
 *   A valid language code on success, FALSE otherwise.
 */
function locale_language_from_browser($languages) {
132 133 134 135 136 137 138 139 140 141
  if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
    return FALSE;
  }

  // The Accept-Language header contains information about the language
  // preferences configured in the user's browser / operating system.
  // RFC 2616 (section 14.4) defines the Accept-Language header as follows:
  //   Accept-Language = "Accept-Language" ":"
  //                  1#( language-range [ ";" "q" "=" qvalue ] )
  //   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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
  $browser_langcodes = array();
  if (preg_match_all('@([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
    foreach ($matches as $match) {
      // We can safely use strtolower() here, tags are ASCII.
      // RFC2616 mandates that the decimal part is no more than three digits,
      // so we multiply the qvalue by 1000 to avoid floating point comparisons.
      $langcode = strtolower($match[1]);
      $qvalue = isset($match[2]) ? (float) $match[2] : 1;
      $browser_langcodes[$langcode] = (int) ($qvalue * 1000);
    }
  }

  // We should take pristine values from the HTTP headers, but Internet Explorer
  // from version 7 sends only specific language tags (eg. fr-CA) without the
  // corresponding generic tag (fr) unless explicitly configured. In that case,
  // we assume that the lowest value of the specific tags is the value of the
  // generic language to be as close to the HTTP 1.1 spec as possible.
  // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 and
  // http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
  asort($browser_langcodes);
  foreach ($browser_langcodes as $langcode => $qvalue) {
    $generic_tag = strtok($langcode, '-');
    if (!isset($browser_langcodes[$generic_tag])) {
      $browser_langcodes[$generic_tag] = $qvalue;
167 168 169
    }
  }

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
  // Find the enabled language with the greatest qvalue, following the rules
  // of RFC 2616 (section 14.4). If several languages have the same qvalue,
  // prefer the one with the greatest weight.
  $best_match_langcode = FALSE;
  $max_qvalue = 0;
  foreach ($languages as $langcode => $language) {
    // Language tags are case insensitive (RFC2616, sec 3.10).
    $langcode = strtolower($langcode);

    // If nothing matches below, the default qvalue is the one of the wildcard
    // language, if set, or is 0 (which will never match).
    $qvalue = isset($browser_langcodes['*']) ? $browser_langcodes['*'] : 0;

    // Find the longest possible prefix of the browser-supplied language
    // ('the language-range') that matches this site language ('the language tag').
    $prefix = $langcode;
    do {
      if (isset($browser_langcodes[$prefix])) {
        $qvalue = $browser_langcodes[$prefix];
        break;
      }
    }
    while ($prefix = substr($prefix, 0, strrpos($prefix, '-')));
193

194 195
    // Find the best match.
    if ($qvalue > $max_qvalue) {
196
      $best_match_langcode = $language->langcode;
197
      $max_qvalue = $qvalue;
198 199 200
    }
  }

201
  return $best_match_langcode;
202 203 204 205 206 207 208 209 210
}

/**
 * Identify language from the user preferences.
 *
 * @param $languages
 *   An array of valid language objects.
 *
 * @return
211
 *   A valid language code on success, FALSE otherwise.
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
 */
function locale_language_from_user($languages) {
  // User preference (only for logged users).
  global $user;

  if ($user->uid) {
    return $user->language;
  }

  // No language preference from the user.
  return FALSE;
}

/**
 * Identify language from a request/session parameter.
 *
 * @param $languages
 *   An array of valid language objects.
 *
 * @return
232
 *   A valid language code on success, FALSE otherwise.
233 234 235 236
 */
function locale_language_from_session($languages) {
  $param = variable_get('locale_language_negotiation_session_param', 'language');

237 238
  // Request parameter: we need to update the session parameter only if we have
  // an authenticated user.
239
  if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) {
240 241 242 243 244
    global $user;
    if ($user->uid) {
      $_SESSION[$param] = $langcode;
    }
    return $langcode;
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
  }

  // Session parameter.
  if (isset($_SESSION[$param])) {
    return $_SESSION[$param];
  }

  return FALSE;
}

/**
 * Identify language via URL prefix or domain.
 *
 * @param $languages
 *   An array of valid language objects.
 *
 * @return
262
 *   A valid language code on success, FALSE otherwise.
263 264 265 266
 */
function locale_language_from_url($languages) {
  $language_url = FALSE;

267
  if (!language_negotiation_get_any(LANGUAGE_NEGOTIATION_URL)) {
268 269 270
    return $language_url;
  }

271 272
  switch (variable_get('locale_language_negotiation_url_part', LANGUAGE_NEGOTIATION_URL_PREFIX)) {
    case LANGUAGE_NEGOTIATION_URL_PREFIX:
273 274 275 276
      // $_GET['q'] might not be available at this time, because
      // path initialization runs after the language bootstrap phase.
      list($language, $_GET['q']) = language_url_split_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages);
      if ($language !== FALSE) {
277
        $language_url = $language->langcode;
278 279 280
      }
      break;

281
    case LANGUAGE_NEGOTIATION_URL_DOMAIN:
282
      $domains = locale_language_negotiation_url_domains();
283
      foreach ($languages as $language) {
284
        // Skip check if the language doesn't have a domain.
285
        if (!empty($domains[$language->langcode])) {
286 287
          // Only compare the domains not the protocols or ports.
          // Remove protocol and add http:// so parse_url works
288
          $host = 'http://' . str_replace(array('http://', 'https://'), '', $domains[$language->langcode]);
289 290
          $host = parse_url($host, PHP_URL_HOST);
          if ($_SERVER['HTTP_HOST'] == $host) {
291
            $language_url = $language->langcode;
292 293
            break;
          }
294 295 296 297 298 299 300 301
        }
      }
      break;
  }

  return $language_url;
}

302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
/**
 * Determines the language to be assigned to URLs when none is detected.
 *
 * The language negotiation process has a fallback chain that ends with the
 * default language provider. Each built-in language type has a separate
 * initialization:
 * - Interface language, which is the only configurable one, always gets a valid
 *   value. If no request-specific language is detected, the default language
 *   will be used.
 * - Content language merely inherits the interface language by default.
 * - URL language is detected from the requested URL and will be used to rewrite
 *   URLs appearing in the page being rendered. If no language can be detected,
 *   there are two possibilities:
 *   - If the default language has no configured path prefix or domain, then the
 *     default language is used. This guarantees that (missing) URL prefixes are
 *     preserved when navigating through the site.
 *   - If the default language has a configured path prefix or domain, a
 *     requested URL having an empty prefix or domain is an anomaly that must be
 *     fixed. This is done by introducing a prefix or domain in the rendered
 *     page matching the detected interface language.
 *
 * @param $languages
 *   (optional) An array of valid language objects. This is passed by
 *   language_provider_invoke() to every language provider callback, but it is
 *   not actually needed here. Defaults to NULL.
 * @param $language_type
 *   (optional) The language type to fall back to. Defaults to the interface
 *   language.
 *
 * @return
 *   A valid language code.
 */
function locale_language_url_fallback($language = NULL, $language_type = LANGUAGE_TYPE_INTERFACE) {
  $default = language_default();
336
  $prefix = (variable_get('locale_language_negotiation_url_part', LANGUAGE_NEGOTIATION_URL_PREFIX) == LANGUAGE_NEGOTIATION_URL_PREFIX);
337 338 339 340

  // If the default language is not configured to convey language information,
  // a missing URL language information indicates that URL language should be
  // the default one, otherwise we fall back to an already detected language.
341 342
  $domains = locale_language_negotiation_url_domains();
  $prefixes = locale_language_negotiation_url_prefixes();
343 344
  if (($prefix && empty($prefixes[$default->langcode])) || (!$prefix && empty($domains[$default->langcode]))) {
    return $default->langcode;
345 346
  }
  else {
347
    return $GLOBALS[$language_type]->langcode;
348 349 350
  }
}

351
/**
352 353 354
 * Return links for the URL language switcher block.
 *
 * Translation links may be provided by other modules.
355 356
 */
function locale_language_switcher_url($type, $path) {
357 358
  // Get the enabled languages only.
  $languages = language_list(TRUE);
359 360
  $links = array();

361
  foreach ($languages as $language) {
362
    $links[$language->langcode] = array(
363
      'href'       => $path,
364
      'title'      => $language->name,
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
      'language'   => $language,
      'attributes' => array('class' => array('language-link')),
    );
  }

  return $links;
}

/**
 * Return the session language switcher block.
 */
function locale_language_switcher_session($type, $path) {
  drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');

  $param = variable_get('locale_language_negotiation_session_param', 'language');
380
  $language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $GLOBALS[$type]->langcode;
381

382 383
  // Get the enabled languages only.
  $languages = language_list(TRUE);
384 385 386 387 388
  $links = array();

  $query = $_GET;
  unset($query['q']);

389
  foreach ($languages as $language) {
390
    $langcode = $language->langcode;
391 392
    $links[$langcode] = array(
      'href'       => $path,
393
      'title'      => $language->name,
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
      'attributes' => array('class' => array('language-link')),
      'query'      => $query,
    );
    if ($language_query != $langcode) {
      $links[$langcode]['query'][$param] = $langcode;
    }
    else {
      $links[$langcode]['attributes']['class'][] = ' session-active';
    }
  }

  return $links;
}

/**
 * Rewrite URLs for the URL language provider.
 */
function locale_language_url_rewrite_url(&$path, &$options) {
412 413 414 415 416 417 418
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast['languages'] = &drupal_static(__FUNCTION__);
  }
  $languages = &$drupal_static_fast['languages'];

  if (!isset($languages)) {
419 420 421
    // Get the enabled languages only.
    $languages = language_list(TRUE);
    $languages = array_flip(array_keys($languages));
422 423
  }

424 425 426 427 428
  // Language can be passed as an option, or we go for current URL language.
  if (!isset($options['language'])) {
    global $language_url;
    $options['language'] = $language_url;
  }
429
  // We allow only enabled languages here.
430
  elseif (!isset($languages[$options['language']->langcode])) {
431 432 433
    unset($options['language']);
    return;
  }
434 435

  if (isset($options['language'])) {
436 437
    switch (variable_get('locale_language_negotiation_url_part', LANGUAGE_NEGOTIATION_URL_PREFIX)) {
      case LANGUAGE_NEGOTIATION_URL_DOMAIN:
438
        $domains = locale_language_negotiation_url_domains();
439
        if (!empty($domains[$options['language']->langcode])) {
440
          // Ask for an absolute URL with our modified base_url.
441 442
          global $is_https;
          $url_scheme = ($is_https) ? 'https://' : 'http://';
443
          $options['absolute'] = TRUE;
444 445 446 447 448 449 450 451 452
          $options['base_url'] = $url_scheme . $domains[$options['language']->langcode];
          if (isset($options['https']) && variable_get('https', FALSE)) {
            if ($options['https'] === TRUE) {
              $options['base_url'] = str_replace('http://', 'https://', $options['base_url']);
            }
            elseif ($options['https'] === FALSE) {
              $options['base_url'] = str_replace('https://', 'http://', $options['base_url']);
            }
          }
453 454 455
        }
        break;

456
      case LANGUAGE_NEGOTIATION_URL_PREFIX:
457
        $prefixes = locale_language_negotiation_url_prefixes();
458 459
        if (!empty($prefixes[$options['language']->langcode])) {
          $options['prefix'] = $prefixes[$options['language']->langcode] . '/';
460 461 462 463 464 465
        }
        break;
    }
  }
}

466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
/**
 * Reads language prefixes and uses the langcode if no prefix is set.
 */
function locale_language_negotiation_url_prefixes() {
  return variable_get('locale_language_negotiation_url_prefixes', array());
}

/**
 * Saves language prefix settings.
 */
function locale_language_negotiation_url_prefixes_save(array $prefixes) {
  variable_set('locale_language_negotiation_url_prefixes', $prefixes);
}

/**
 * Reads language domains.
 */
function locale_language_negotiation_url_domains() {
  return variable_get('locale_language_negotiation_url_domains', array());
}

/**
 * Saves the language domain settings.
 */
function locale_language_negotiation_url_domains_save(array $domains) {
  variable_set('locale_language_negotiation_url_domains', $domains);
}

494 495 496 497 498 499 500 501 502 503 504
/**
 * Rewrite URLs for the Session language provider.
 */
function locale_language_url_rewrite_session(&$path, &$options) {
  static $query_rewrite, $query_param, $query_value;

  // The following values are not supposed to change during a single page
  // request processing.
  if (!isset($query_rewrite)) {
    global $user;
    if (!$user->uid) {
505 506
      // Get the enabled languages only.
      $languages = language_list(TRUE);
507 508
      $query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language'));
      $query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL;
509
      $query_rewrite = isset($languages[$query_value]) && language_negotiation_get_any(LANGUAGE_NEGOTIATION_SESSION);
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
    }
    else {
      $query_rewrite = FALSE;
    }
  }

  // If the user is anonymous, the user language provider is enabled, and the
  // corresponding option has been set, we must preserve any explicit user
  // language preference even with cookies disabled.
  if ($query_rewrite) {
    if (is_string($options['query'])) {
      $options['query'] = drupal_get_query_array($options['query']);
    }
    if (!isset($options['query'][$query_param])) {
      $options['query'][$query_param] = $query_value;
    }
  }
}

529 530 531 532
/**
 * @} End of "locale-languages-negotiation"
 */

533 534 535 536 537 538
/**
 * Check that a string is safe to be added or imported as a translation.
 *
 * This test can be used to detect possibly bad translation strings. It should
 * not have any false positives. But it is only a test, not a transformation,
 * as it destroys valid HTML. We cannot reliably filter translation strings
539
 * on import because some strings are irreversibly corrupted. For example,
540 541 542 543 544 545 546 547 548 549 550
 * a &amp; in the translation would get encoded to &amp;amp; by filter_xss()
 * before being put in the database, and thus would be displayed incorrectly.
 *
 * The allowed tag list is like filter_xss_admin(), but omitting div and img as
 * not needed for translation and likely to cause layout issues (div) or a
 * possible attack vector (img).
 */
function locale_string_is_safe($string) {
  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')));
}

551 552 553 554 555
/**
 * Parses a JavaScript file, extracts strings wrapped in Drupal.t() and
 * Drupal.formatPlural() and inserts them into the database.
 */
function _locale_parse_js_file($filepath) {
556 557 558 559
  // The file path might contain a query string, so make sure we only use the
  // actual file.
  $parsed_url = drupal_parse_url($filepath);
  $filepath = $parsed_url['path'];
560 561 562 563 564
  // Load the JavaScript file.
  $file = file_get_contents($filepath);

  // Match all calls to Drupal.t() in an array.
  // Note: \s also matches newlines with the 's' modifier.
565 566 567 568
  preg_match_all('~
    [^\w]Drupal\s*\.\s*t\s*                       # match "Drupal.t" with whitespace
    \(\s*                                         # match "(" argument list start
    (' . LOCALE_JS_STRING . ')\s*                 # capture string argument
569 570 571
    (?:,\s*' . LOCALE_JS_OBJECT . '\s*            # optionally capture str args
      (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*) # optionally capture context
    ?)?                                           # close optional args
572 573
    [,\)]                                         # match ")" or "," to finish
    ~sx', $file, $t_matches);
574 575

  // Match all Drupal.formatPlural() calls in another array.
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
  preg_match_all('~
    [^\w]Drupal\s*\.\s*formatPlural\s*  # match "Drupal.formatPlural" with whitespace
    \(                                  # match "(" argument list start
    \s*.+?\s*,\s*                       # match count argument
    (' . LOCALE_JS_STRING . ')\s*,\s*   # match singular string argument
    (                             # capture plural string argument
      (?:                         # non-capturing group to repeat string pieces
        (?:
          \'                      # match start of single-quoted string
          (?:\\\\\'|[^\'])*       # match any character except unescaped single-quote
          @count                  # match "@count"
          (?:\\\\\'|[^\'])*       # match any character except unescaped single-quote
          \'                      # match end of single-quoted string
          |
          "                       # match start of double-quoted string
          (?:\\\\"|[^"])*         # match any character except unescaped double-quote
          @count                  # match "@count"
          (?:\\\\"|[^"])*         # match any character except unescaped double-quote
          "                       # match end of double-quoted string
        )
        (?:\s*\+\s*)?             # match "+" with possible whitespace, for str concat
      )+                          # match multiple because we supports concatenating strs
    )\s*                          # end capturing of plural string argument
599 600 601
    (?:,\s*' . LOCALE_JS_OBJECT . '\s*          # optionally capture string args
      (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*)?  # optionally capture context
    )?
602 603 604
    [,\)]
    ~sx', $file, $plural_matches);

605
  $matches = array();
606

607 608 609 610 611 612 613 614 615 616 617 618 619 620
  // Add strings from Drupal.t().
  foreach ($t_matches[1] as $key => $string) {
    $matches[] = array(
      'string'  => $string,
      'context' => $t_matches[2][$key],
    );
  }

  // Add string from Drupal.formatPlural().
  foreach ($plural_matches[1] as $key => $string) {
    $matches[] = array(
      'string'  => $string,
      'context' => $plural_matches[3][$key],
    );
621 622 623

    // If there is also a plural version of this string, add it to the strings array.
    if (isset($plural_matches[2][$key])) {
624 625 626 627
      $matches[] = array(
        'string'  => $plural_matches[2][$key],
        'context' => $plural_matches[3][$key],
      );
628
    }
629
  }
630

631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
  // Loop through all matches and process them.
  foreach ($matches as $key => $match) {

    // Remove the quotes and string concatenations from the string and context.
    $string =  implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['string'], 1, -1)));
    $context = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['context'], 1, -1)));

    $source = db_query("SELECT lid, location FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $string, ':context' => $context))->fetchObject();
    if ($source) {
      // We already have this source string and now have to add the location
      // to the location column, if this file is not yet present in there.
      $locations = preg_split('~\s*;\s*~', $source->location);

      if (!in_array($filepath, $locations)) {
        $locations[] = $filepath;
        $locations = implode('; ', $locations);

        // Save the new locations string to the database.
        db_update('locales_source')
650
          ->fields(array(
651
            'location' => $locations,
652
          ))
653
          ->condition('lid', $source->lid)
654
          ->execute();
655 656
      }
    }
657 658 659 660 661 662 663 664 665 666
    else {
      // We don't have the source string yet, thus we insert it into the database.
      db_insert('locales_source')
        ->fields(array(
          'location'  => $filepath,
          'source'    => $string,
          'context'   => $context,
        ))
        ->execute();
    }
667 668 669
  }
}

670 671 672 673 674 675 676 677 678 679
/**
 * Force the JavaScript translation file(s) to be refreshed.
 *
 * This function sets a refresh flag for a specified language, or all
 * languages except English, if none specified. JavaScript translation
 * files are rebuilt (with locale_update_js_files()) the next time a
 * request is served in that language.
 *
 * @param $langcode
 *   The language code for which the file needs to be refreshed.
680
 *
681 682 683 684 685 686 687 688 689
 * @return
 *   New content of the 'javascript_parsed' variable.
 */
function _locale_invalidate_js($langcode = NULL) {
  $parsed = variable_get('javascript_parsed', array());

  if (empty($langcode)) {
    // Invalidate all languages.
    $languages = language_list();
690 691 692
    if (!locale_translate_english()) {
      unset($languages['en']);
    }
693
    foreach ($languages as $lcode => $data) {
694
      $parsed['refresh:' . $lcode] = 'waiting';
695 696 697 698
    }
  }
  else {
    // Invalidate single language.
699
    $parsed['refresh:' . $langcode] = 'waiting';
700 701 702 703 704 705
  }

  variable_set('javascript_parsed', $parsed);
  return $parsed;
}

706 707 708
/**
 * (Re-)Creates the JavaScript translation file for a language.
 *
709
 * @param $langcode
710 711 712 713
 *   The language, the translation file should be (re)created for.
 */
function _locale_rebuild_js($langcode = NULL) {
  if (!isset($langcode)) {
714 715
    global $language_interface;
    $language = $language_interface;
716 717 718 719 720 721 722 723
  }
  else {
    // Get information about the locale.
    $languages = language_list();
    $language = $languages[$langcode];
  }

  // Construct the array for JavaScript translations.
724
  // Only add strings with a translation to the translations array.
725
  $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%'", array(':language' => $language->langcode));
726

727
  $translations = array();
728
  foreach ($result as $data) {
729
    $translations[$data->context][$data->source] = $data->translation;
730 731
  }

732
  // Construct the JavaScript file, if there are translations.
733
  $data_hash = NULL;
734
  $data = $status = '';
735
  if (!empty($translations)) {
736

737 738
    $data = "Drupal.locale = { ";

739
    $locale_plurals = variable_get('locale_translation_plurals', array());
740 741
    if (!empty($locale_plurals[$language->langcode])) {
      $data .= "'pluralFormula': function (\$n) { return Number({$locale_plurals[$language->langcode]['formula']}); }, ";
742 743
    }

744
    $data .= "'strings': " . drupal_json_encode($translations) . " };";
745
    $data_hash = drupal_hash_base64($data);
746
  }
747

748 749
  // Construct the filepath where JS translation files are stored.
  // There is (on purpose) no front end to edit that variable.
750
  $dir = 'public://' . variable_get('locale_js_directory', 'languages');
751

752
  // Delete old file, if we have no translations anymore, or a different file to be saved.
753
  $locale_javascripts = variable_get('locale_translation_javascript', array());
754 755 756 757
  $changed_hash = !isset($locale_javascripts[$language->langcode]) || ($locale_javascripts[$language->langcode] != $data_hash);
  if (!empty($locale_javascripts[$language->langcode]) && (!$data || $changed_hash)) {
    file_unmanaged_delete($dir . '/' . $language->langcode . '_' . $locale_javascripts[$language->langcode] . '.js');
    $locale_javascripts[$language->langcode] = '';
758 759
    $status = 'deleted';
  }
760

761 762
  // Only create a new file if the content has changed or the original file got
  // lost.
763
  $dest = $dir . '/' . $language->langcode . '_' . $data_hash . '.js';
764
  if ($data && ($changed_hash || !file_exists($dest))) {
765
    // Ensure that the directory exists and is writable, if possible.
766
    file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
767

768
    // Save the file.
769
    if (file_unmanaged_save_data($data, $dest)) {
770
      $locale_javascripts[$language->langcode] = $data_hash;
771 772 773 774 775 776 777 778 779 780 781 782 783 784 785
      // If we deleted a previous version of the file and we replace it with a
      // new one we have an update.
      if ($status == 'deleted') {
        $status = 'updated';
      }
      // If the file did not exist previously and the data has changed we have
      // a fresh creation.
      elseif ($changed_hash) {
        $status = 'created';
      }
      // If the data hash is unchanged the translation was lost and has to be
      // rebuilt.
      else {
        $status = 'rebuilt';
      }
786 787
    }
    else {
788
      $locale_javascripts[$language->langcode] = '';
789 790 791
      $status = 'error';
    }
  }
792

793 794 795 796
  // Save the new JavaScript hash (or an empty value if the file just got
  // deleted). Act only if some operation was executed that changed the hash
  // code.
  if ($status && $changed_hash) {
797
    variable_set('locale_translation_javascript', $locale_javascripts);
798 799
  }

800 801 802
  // Log the operation and return success flag.
  switch ($status) {
    case 'updated':
803
      watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => $language->name));
804
      return TRUE;
805
    case 'rebuilt':
806
      watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $locale_javascripts[$language->langcode]), WATCHDOG_WARNING);
807 808
      // Proceed to the 'created' case as the JavaScript translation file has
      // been created again.
809
    case 'created':
810
      watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => $language->name));
811 812
      return TRUE;
    case 'deleted':
813
      watchdog('locale', 'Removed JavaScript translation file for the language %language because no translations currently exist for that language.', array('%language' => $language->name));
814 815
      return TRUE;
    case 'error':
816
      watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => $language->name), WATCHDOG_ERROR);
817 818 819 820
      return FALSE;
    default:
      // No operation needed.
      return TRUE;
821 822 823
  }
}

824 825 826 827 828 829 830
/**
 * Get list of all predefined and custom countries.
 *
 * @return
 *   An array of all country code => country name pairs.
 */
function country_get_list() {
831
  include_once DRUPAL_ROOT . '/core/includes/standard.inc';
832
  $countries = standard_country_list();
833 834 835 836 837
  // Allow other modules to modify the country list.
  drupal_alter('countries', $countries);
  return $countries;
}

838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
/**
 * Save locale specific date formats to the database.
 *
 * @param $langcode
 *   Language code, can be 2 characters, e.g. 'en' or 5 characters, e.g.
 *   'en-CA'.
 * @param $type
 *   Date format type, e.g. 'short', 'medium'.
 * @param $format
 *   The date format string.
 */
function locale_date_format_save($langcode, $type, $format) {
  $locale_format = array();
  $locale_format['language'] = $langcode;
  $locale_format['type'] = $type;
  $locale_format['format'] = $format;

  $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();
  if ($is_existing) {
    $keys = array('type', 'language');
    drupal_write_record('date_format_locale', $locale_format, $keys);
  }
  else {
    drupal_write_record('date_format_locale', $locale_format);
  }
}

/**
 * Select locale date format details from database.
 *
 * @param $languages
 *   An array of language codes.
870
 *
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921
 * @return
 *   An array of date formats.
 */
function locale_get_localized_date_format($languages) {
  $formats = array();

  // Get list of different format types.
  $format_types = system_get_date_types();
  $short_default = variable_get('date_format_short', 'm/d/Y - H:i');

  // Loop through each language until we find one with some date formats
  // configured.
  foreach ($languages as $language) {
    $date_formats = system_date_format_locale($language);
    if (!empty($date_formats)) {
      // We have locale-specific date formats, so check for their types. If
      // we're missing a type, use the default setting instead.
      foreach ($format_types as $type => $type_info) {
        // If format exists for this language, use it.
        if (!empty($date_formats[$type])) {
          $formats['date_format_' . $type] = $date_formats[$type];
        }
        // Otherwise get default variable setting. If this is not set, default
        // to the short format.
        else {
          $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default);
        }
      }

      // Return on the first match.
      return $formats;
    }
  }

  // No locale specific formats found, so use defaults.
  $system_types = array('short', 'medium', 'long');
  // Handle system types separately as they have defaults if no variable exists.
  $formats['date_format_short'] = $short_default;
  $formats['date_format_medium'] = variable_get('date_format_medium', 'D, m/d/Y - H:i');
  $formats['date_format_long'] = variable_get('date_format_long', 'l, F j, Y - H:i');

  // For non-system types, get the default setting, otherwise use the short
  // format.
  foreach ($format_types as $type => $type_info) {
    if (!in_array($type, $system_types)) {
      $formats['date_format_' . $type] = variable_get('date_format_' . $type, $short_default);
    }
  }

  return $formats;
}