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