locale.inc 32.4 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 12 13 14 15 16 17 18 19 20 21 22 23
/**
 * The language is determined using a URL language indicator:
 * path prefix or domain according to the configuration.
 */
define('LOCALE_LANGUAGE_NEGOTIATION_URL', 'locale-url');

/**
 * The language is set based on the browser language settings.
 */
define('LOCALE_LANGUAGE_NEGOTIATION_BROWSER', 'locale-browser');

/**
 * The language is determined using the current interface language.
 */
define('LOCALE_LANGUAGE_NEGOTIATION_INTERFACE', 'locale-interface');

24 25 26 27 28 29
/**
 * If no URL language is available language is determined using an already
 * detected one.
 */
define('LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK', 'locale-url-fallback');

30 31 32 33 34 35 36 37 38 39
/**
 * The language is set based on the user language settings.
 */
define('LOCALE_LANGUAGE_NEGOTIATION_USER', 'locale-user');

/**
 * The language is set based on the request/session parameters.
 */
define('LOCALE_LANGUAGE_NEGOTIATION_SESSION', 'locale-session');

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

45 46 47 48 49 50 51 52 53 54 55 56
/**
 * Translation import mode overwriting all existing translations
 * if new translated version available.
 */
define('LOCALE_IMPORT_OVERWRITE', 0);

/**
 * Translation import mode keeping existing translations and only
 * inserting new strings.
 */
define('LOCALE_IMPORT_KEEP', 1);

57 58 59 60 61 62 63 64 65 66 67 68
/**
 * URL language negotiation: use the path prefix as URL language
 * indicator.
 */
define('LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX', 0);

/**
 * URL language negotiation: use the domain as URL language
 * indicator.
 */
define('LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN', 1);

69
/**
70
 * @defgroup locale-languages-negotiation Language negotiation options
71
 * @{
72 73 74 75
 * Functions for language negotiation.
 *
 * There are functions that provide the ability to identify the
 * language. This behavior can be controlled by various options.
76
 */
77

78
/**
79
 * Identifies the language from the current interface language.
80 81
 *
 * @return
82
 *   The current interface language code.
83
 */
84
function locale_language_from_interface() {
85 86 87 88 89 90 91 92 93 94 95
  global $language;
  return isset($language->language) ? $language->language : FALSE;
}

/**
 * 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
96
 *   An array of language objects for enabled languages ordered by weight.
97 98 99 100 101
 *
 * @return
 *   A valid language code on success, FALSE otherwise.
 */
function locale_language_from_browser($languages) {
102 103 104 105 106 107 108 109 110 111
  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 ) ) | "*" )
112
  // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
  $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;
137 138 139
    }
  }

140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
  // 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, '-')));
163

164 165 166 167
    // Find the best match.
    if ($qvalue > $max_qvalue) {
      $best_match_langcode = $language->language;
      $max_qvalue = $qvalue;
168 169 170
    }
  }

171
  return $best_match_langcode;
172 173 174 175 176 177 178 179 180
}

/**
 * Identify language from the user preferences.
 *
 * @param $languages
 *   An array of valid language objects.
 *
 * @return
181
 *   A valid language code on success, FALSE otherwise.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
 */
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
202
 *   A valid language code on success, FALSE otherwise.
203 204 205 206
 */
function locale_language_from_session($languages) {
  $param = variable_get('locale_language_negotiation_session_param', 'language');

207 208
  // Request parameter: we need to update the session parameter only if we have
  // an authenticated user.
209
  if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) {
210 211 212 213 214
    global $user;
    if ($user->uid) {
      $_SESSION[$param] = $langcode;
    }
    return $langcode;
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
  }

  // 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
232
 *   A valid language code on success, FALSE otherwise.
233 234 235 236
 */
function locale_language_from_url($languages) {
  $language_url = FALSE;

237 238 239 240
  if (!language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) {
    return $language_url;
  }

241 242 243 244 245 246 247 248 249 250 251 252
  switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
    case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
      // $_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) {
        $language_url = $language->language;
      }
      break;

    case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
      foreach ($languages as $language) {
253 254 255 256 257 258 259 260 261 262
        // Skip check if the language doesn't have a domain.
        if ($language->domain) {
          // Only compare the domains not the protocols or ports.
          // Remove protocol and add http:// so parse_url works
          $host = 'http://' . str_replace(array('http://', 'https://'), '', $language->domain);
          $host = parse_url($host, PHP_URL_HOST);
          if ($_SERVER['HTTP_HOST'] == $host) {
            $language_url = $language->language;
            break;
          }
263 264 265 266 267 268 269 270
        }
      }
      break;
  }

  return $language_url;
}

271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
/**
 * 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();
  $prefix = (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX);

  // 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.
  if (($prefix && empty($default->prefix)) || (!$prefix && empty($default->domain))) {
    return $default->language;
  }
  else {
    return $GLOBALS[$language_type]->language;
  }
}

318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
/**
 * Return the URL language switcher block. Translation links may be provided by
 * other modules.
 */
function locale_language_switcher_url($type, $path) {
  $languages = language_list('enabled');
  $links = array();

  foreach ($languages[1] as $language) {
    $links[$language->language] = array(
      'href'       => $path,
      'title'      => $language->native,
      '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');
  $language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $GLOBALS[$type]->language;

  $languages = language_list('enabled');
  $links = array();

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

  foreach ($languages[1] as $language) {
    $langcode = $language->language;
    $links[$langcode] = array(
      'href'       => $path,
      'title'      => $language->native,
      '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) {
376 377 378 379 380 381 382 383 384 385 386
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast['languages'] = &drupal_static(__FUNCTION__);
  }
  $languages = &$drupal_static_fast['languages'];

  if (!isset($languages)) {
    $languages = language_list('enabled');
    $languages = array_flip(array_keys($languages[1]));
  }

387 388 389 390 391
  // 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;
  }
392 393 394 395 396
  // We allow only enabled languages here.
  elseif (!isset($languages[$options['language']->language])) {
    unset($options['language']);
    return;
  }
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451

  if (isset($options['language'])) {
    switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
      case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
        if ($options['language']->domain) {
          // Ask for an absolute URL with our modified base_url.
          $options['absolute'] = TRUE;
          $options['base_url'] = $options['language']->domain;
        }
        break;

      case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
        if (!empty($options['language']->prefix)) {
          $options['prefix'] = $options['language']->prefix . '/';
        }
        break;
    }
  }
}

/**
 * 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) {
      $languages = language_list('enabled');
      $languages = $languages[1];
      $query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language'));
      $query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL;
      $query_rewrite = isset($languages[$query_value]) && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION);
    }
    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;
    }
  }
}

452 453 454 455
/**
 * @} End of "locale-languages-negotiation"
 */

456 457 458 459 460 461
/**
 * 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
462
 * on import because some strings are irreversibly corrupted. For example,
463 464 465 466 467 468 469 470 471 472 473
 * 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')));
}

474
/**
475
 * API function to add or update a language.
476
 *
477 478
 * @param $language
 *   Language object with properties corresponding to 'languages' table columns.
479
 */
480 481 482 483 484
function locale_language_save($language) {
  $language->is_new = !(bool) db_query_range('SELECT 1 FROM {languages} WHERE language = :language', 0, 1, array(':language' => $language->language))->fetchField();
  // Default prefix on language code if not provided otherwise.
  if (!isset($language->prefix)) {
    $language->prefix = $language->language;
485 486
  }

487
  // If name was not set, we add a predefined language.
488
  if (!isset($language->name)) {
489 490
    include_once DRUPAL_ROOT . '/includes/standard.inc';
    $predefined = standard_language_list();
491 492 493
    $language->name = $predefined[$language->language][0];
    $language->native = isset($predefined[$language->language][1]) ? $predefined[$language->language][1] : $predefined[$language->language][0];
    $language->direction = isset($predefined[$language->language][2]) ? $predefined[$language->language][2] : LANGUAGE_LTR;
494 495
  }

496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
  // Set to enabled for the default language and unless specified otherwise.
  if (!empty($language->default) || !isset($language->enabled)) {
    $language->enabled = TRUE;
  }
  // Let other modules modify $language before saved.
  module_invoke_all('locale_language_presave', $language);

  // Save the record and inform others about the change.
  if ($language->is_new) {
    drupal_write_record('languages', $language);
    module_invoke_all('locale_language_insert', $language);
    watchdog('locale', 'The %language (%langcode) language has been created.', array('%language' => $language->name, '%langcode' => $language->language));
  }
  else {
    drupal_write_record('languages', $language, array('language'));
    module_invoke_all('locale_language_update', $language);
    watchdog('locale', 'The %language (%langcode) language has been updated.', array('%language' => $language->name, '%langcode' => $language->language));
513 514
  }

515 516 517 518
  if (!empty($language->default)) {
    // Set the new version of this language as default in a variable.
    $default_language = language_default();
    variable_set('language_default', $language);
519
  }
520

521 522 523
  // Update language count based on enabled language count.
  variable_set('language_count', db_query('SELECT COUNT(language) FROM {languages} WHERE enabled = 1')->fetchField());

524 525 526
  // Kill the static cache in language_list().
  drupal_static_reset('language_list');

527 528 529 530 531
  // @todo move these two cache clears out. See http://drupal.org/node/1293252
  // Changing the language settings impacts the interface.
  cache_clear_all('*', 'cache_page', TRUE);
  // Force JavaScript translation file re-creation for the modified language.
  _locale_invalidate_js($language->language);
532

533
  return $language;
534 535
}

536 537 538 539 540 541 542
/**
 * 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) {
  global $language;

543 544 545 546
  // 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'];
547 548 549 550 551
  // 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.
552 553 554 555 556 557
  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
    [,\)]                                         # match ")" or "," to finish
    ~sx', $file, $t_matches);
558 559

  // Match all Drupal.formatPlural() calls in another array.
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
  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
    [,\)]
    ~sx', $file, $plural_matches);

586 587 588 589 590 591 592 593 594 595 596 597 598 599 600

  // Loop through all matches and process them.
  $all_matches = array_merge($plural_matches[1], $t_matches[1]);
  foreach ($all_matches as $key => $string) {
    $strings = array($string);

    // If there is also a plural version of this string, add it to the strings array.
    if (isset($plural_matches[2][$key])) {
      $strings[] = $plural_matches[2][$key];
    }

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

601
      $source = db_query("SELECT lid, location FROM {locales_source} WHERE source = :source", array(':source' => $string))->fetchObject();
602
      if ($source) {
603 604 605 606 607 608 609 610 611
        // 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.
612 613 614 615 616 617
          db_update('locales_source')
            ->fields(array(
              'location' => $locations,
            ))
            ->condition('lid', $source->lid)
            ->execute();
618 619 620 621
        }
      }
      else {
        // We don't have the source string yet, thus we insert it into the database.
622 623 624 625
        db_insert('locales_source')
          ->fields(array(
            'location' => $filepath,
            'source' => $string,
626
            'context' => '',
627 628
          ))
          ->execute();
629 630 631 632 633
      }
    }
  }
}

634 635 636 637 638 639 640 641 642 643
/**
 * 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.
644
 *
645 646 647 648 649 650 651 652 653 654 655
 * @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();
    unset($languages['en']);
    foreach ($languages as $lcode => $data) {
656
      $parsed['refresh:' . $lcode] = 'waiting';
657 658 659 660
    }
  }
  else {
    // Invalidate single language.
661
    $parsed['refresh:' . $langcode] = 'waiting';
662 663 664 665 666 667
  }

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

668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
/**
 * (Re-)Creates the JavaScript translation file for a language.
 *
 * @param $language
 *   The language, the translation file should be (re)created for.
 */
function _locale_rebuild_js($langcode = NULL) {
  if (!isset($langcode)) {
    global $language;
  }
  else {
    // Get information about the locale.
    $languages = language_list();
    $language = $languages[$langcode];
  }

  // Construct the array for JavaScript translations.
685
  // Only add strings with a translation to the translations array.
686
  $result = db_query("SELECT s.lid, s.source, 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->language));
687

688
  $translations = array();
689
  foreach ($result as $data) {
690
    $translations[$data->source] = $data->translation;
691 692
  }

693
  // Construct the JavaScript file, if there are translations.
694
  $data_hash = NULL;
695
  $data = $status = '';
696
  if (!empty($translations)) {
697

698 699 700
    $data = "Drupal.locale = { ";

    if (!empty($language->formula)) {
701
      $data .= "'pluralFormula': function (\$n) { return Number({$language->formula}); }, ";
702 703
    }

704
    $data .= "'strings': " . drupal_json_encode($translations) . " };";
705
    $data_hash = drupal_hash_base64($data);
706
  }
707

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

712
  // Delete old file, if we have no translations anymore, or a different file to be saved.
713 714
  $changed_hash = $language->javascript != $data_hash;
  if (!empty($language->javascript) && (!$data || $changed_hash)) {
715
    file_unmanaged_delete($dir . '/' . $language->language . '_' . $language->javascript . '.js');
716 717 718
    $language->javascript = '';
    $status = 'deleted';
  }
719

720 721 722 723
  // Only create a new file if the content has changed or the original file got
  // lost.
  $dest = $dir . '/' . $language->language . '_' . $data_hash . '.js';
  if ($data && ($changed_hash || !file_exists($dest))) {
724
    // Ensure that the directory exists and is writable, if possible.
725
    file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
726

727
    // Save the file.
728
    if (file_unmanaged_save_data($data, $dest)) {
729
      $language->javascript = $data_hash;
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
      // 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';
      }
745 746 747 748 749 750
    }
    else {
      $language->javascript = '';
      $status = 'error';
    }
  }
751

752 753 754 755
  // 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) {
756
    db_update('languages')
757
      ->fields(array(
758 759 760 761
        'javascript' => $language->javascript,
      ))
      ->condition('language', $language->language)
      ->execute();
762 763 764 765

    // Update the default language variable if the default language has been altered.
    // This is necessary to keep the variable consistent with the database
    // version of the language and to prevent checking against an outdated hash.
766
    $default_langcode = language_default()->language;
767
    if ($default_langcode == $language->language) {
768
      $default = db_query("SELECT * FROM {languages} WHERE language = :language", array(':language' => $default_langcode))->fetchObject();
769
      variable_set('language_default', $default);
770 771 772
    }
  }

773 774 775 776 777
  // Log the operation and return success flag.
  switch ($status) {
    case 'updated':
      watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => t($language->name)));
      return TRUE;
778
    case 'rebuilt':
779
      watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $language->javascript), WATCHDOG_WARNING);
780 781
      // Proceed to the 'created' case as the JavaScript translation file has
      // been created again.
782 783 784 785
    case 'created':
      watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => t($language->name)));
      return TRUE;
    case 'deleted':
786
      watchdog('locale', 'Removed JavaScript translation file for the language %language because no translations currently exist for that language.', array('%language' => t($language->name)));
787 788
      return TRUE;
    case 'error':
789
      watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => t($language->name)), WATCHDOG_ERROR);
790 791 792 793
      return FALSE;
    default:
      // No operation needed.
      return TRUE;
794 795 796
  }
}

797 798 799
/**
 * @defgroup locale-api-predefined List of predefined languages
 * @{
800
 * API to provide a list of predefined languages.
801
 */
Dries's avatar
 
Dries committed
802 803 804 805

/**
 * Prepares the language code list for a select form item with only the unsupported ones
 */
806
function _locale_prepare_predefined_list() {
807
  include_once DRUPAL_ROOT . '/includes/standard.inc';
808
  $languages = language_list();
809
  $predefined = standard_language_list();
810 811 812
  foreach ($predefined as $key => $value) {
    if (isset($languages[$key])) {
      unset($predefined[$key]);
Dries's avatar
 
Dries committed
813 814
      continue;
    }
815
    $predefined[$key] = t($value[0]);
Dries's avatar
 
Dries committed
816
  }
817 818
  asort($predefined);
  return $predefined;
Dries's avatar
 
Dries committed
819 820
}

821 822 823
/**
 * @} End of "locale-api-languages-predefined"
 */
824

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

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 870
/**
 * 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.
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 922
 * @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;
}