Projet

Général

Profil

Paste
Télécharger (84,4 ko) Statistiques
| Branche: | Révision:

root / drupal7 / includes / locale.inc @ c7768a53

1
<?php
2

    
3
/**
4
 * @file
5
 * Administration functions for locale.module.
6
 */
7

    
8
/**
9
 * The language is determined using a URL language indicator:
10
 * path prefix or domain according to the configuration.
11
 */
12
define('LOCALE_LANGUAGE_NEGOTIATION_URL', 'locale-url');
13

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

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

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

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

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

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

    
45
/**
46
 * Regular expression pattern used to match simple JS object literal.
47
 *
48
 * This pattern matches a basic JS object, but will fail on an object with
49
 * nested objects. Used in JS file parsing for string arg processing.
50
 */
51
define('LOCALE_JS_OBJECT', '\{.*?\}');
52

    
53
/**
54
 * Regular expression to match an object containing a key 'context'.
55
 *
56
 * Pattern to match a JS object containing a 'context key' with a string value,
57
 * which is captured. Will fail if there are nested objects.
58
 */
59
define('LOCALE_JS_OBJECT_CONTEXT', '
60
  \{              # match object literal start
61
  .*?             # match anything, non-greedy
62
  (?:             # match a form of "context"
63
    \'context\'
64
    |
65
    "context"
66
    |
67
    context
68
  )
69
  \s*:\s*         # match key-value separator ":"
70
  (' . LOCALE_JS_STRING . ')  # match context string
71
  .*?             # match anything, non-greedy
72
  \}              # match end of object literal
73
');
74

    
75
/**
76
 * Translation import mode overwriting all existing translations
77
 * if new translated version available.
78
 */
79
define('LOCALE_IMPORT_OVERWRITE', 0);
80

    
81
/**
82
 * Translation import mode keeping existing translations and only
83
 * inserting new strings.
84
 */
85
define('LOCALE_IMPORT_KEEP', 1);
86

    
87
/**
88
 * URL language negotiation: use the path prefix as URL language
89
 * indicator.
90
 */
91
define('LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX', 0);
92

    
93
/**
94
 * URL language negotiation: use the domain as URL language
95
 * indicator.
96
 */
97
define('LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN', 1);
98

    
99
/**
100
 * @defgroup locale-languages-negotiation Language negotiation options
101
 * @{
102
 * Functions for language negotiation.
103
 *
104
 * There are functions that provide the ability to identify the
105
 * language. This behavior can be controlled by various options.
106
 */
107

    
108
/**
109
 * Identifies the language from the current interface language.
110
 *
111
 * @return
112
 *   The current interface language code.
113
 */
114
function locale_language_from_interface() {
115
  global $language;
116
  return isset($language->language) ? $language->language : FALSE;
117
}
118

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

    
136
  // The Accept-Language header contains information about the language
137
  // preferences configured in the user's browser / operating system.
138
  // RFC 2616 (section 14.4) defines the Accept-Language header as follows:
139
  //   Accept-Language = "Accept-Language" ":"
140
  //                  1#( language-range [ ";" "q" "=" qvalue ] )
141
  //   language-range  = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
142
  // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
143
  $browser_langcodes = array();
144
  if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
145
    foreach ($matches as $match) {
146
      // We can safely use strtolower() here, tags are ASCII.
147
      // RFC2616 mandates that the decimal part is no more than three digits,
148
      // so we multiply the qvalue by 1000 to avoid floating point comparisons.
149
      $langcode = strtolower($match[1]);
150
      $qvalue = isset($match[2]) ? (float) $match[2] : 1;
151
      $browser_langcodes[$langcode] = (int) ($qvalue * 1000);
152
    }
153
  }
154

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

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

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

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

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

    
201
  return $best_match_langcode;
202
}
203

    
204
/**
205
 * Identify language from the user preferences.
206
 *
207
 * @param $languages
208
 *   An array of valid language objects.
209
 *
210
 * @return
211
 *   A valid language code on success, FALSE otherwise.
212
 */
213
function locale_language_from_user($languages) {
214
  // User preference (only for logged users).
215
  global $user;
216

    
217
  if ($user->uid) {
218
    return $user->language;
219
  }
220

    
221
  // No language preference from the user.
222
  return FALSE;
223
}
224

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

    
237
  // Request parameter: we need to update the session parameter only if we have
238
  // an authenticated user.
239
  if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) {
240
    global $user;
241
    if ($user->uid) {
242
      $_SESSION[$param] = $langcode;
243
    }
244
    return $langcode;
245
  }
246

    
247
  // Session parameter.
248
  if (isset($_SESSION[$param])) {
249
    return $_SESSION[$param];
250
  }
251

    
252
  return FALSE;
253
}
254

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

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

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

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

    
304
  return $language_url;
305
}
306

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

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

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

    
362
  foreach ($languages[1] as $language) {
363
    $links[$language->language] = array(
364
      'href'       => $path,
365
      'title'      => $language->native,
366
      'language'   => $language,
367
      'attributes' => array('class' => array('language-link')),
368
    );
369
  }
370

    
371
  return $links;
372
}
373

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

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

    
383
  $languages = language_list('enabled');
384
  $links = array();
385

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

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

    
405
  return $links;
406
}
407

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

    
418
  if (!isset($languages)) {
419
    $languages = language_list('enabled');
420
    $languages = array_flip(array_keys($languages[1]));
421
  }
422

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

    
434
  if (isset($options['language'])) {
435
    switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
436
      case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
437
        if ($options['language']->domain) {
438
          // 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
 */
495
function 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
 */
545
function 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
 */
595
function 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
 */
669
function _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
 */
723
function _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
 */
987
function _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
 */
1015
function _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
 */
1119
function _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
 */
1218
function _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
 */
1242
function _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
 */
1286
function _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
 */
1386
function _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
 */
1452
function _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
 */
1472
function _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
 */
1495
function _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
 */
1519
function _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
 */
1652
function _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
 */
1693
function _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
 */
1785
function _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
 */
1805
function _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
 */
1841
function _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
 */
1869
function _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
 */
1888
function _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
 */
1988
function _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
 */
2016
function _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
 */
2042
function _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
 */
2168
function _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
 */
2201
function _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
 */
2251
function 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
 */
2286
function 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
 */
2321
function _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
 */
2357
function _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
 */
2373
function _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
 */
2385
function _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
 */
2401
function 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
 */
2420
function 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
 */
2445
function 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
}