forked from pantheon-systems/drops-7
-
Notifications
You must be signed in to change notification settings - Fork 0
/
locale.inc
2423 lines (2183 loc) · 81.1 KB
/
locale.inc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/**
* @file
* Administration functions for locale.module.
*/
/**
* The language is determined using a URL language indicator:
* path prefix or domain according to the configuration.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_URL', 'locale-url');
/**
* The language is set based on the browser language settings.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_BROWSER', 'locale-browser');
/**
* The language is determined using the current interface language.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_INTERFACE', 'locale-interface');
/**
* If no URL language is available language is determined using an already
* detected one.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK', 'locale-url-fallback');
/**
* The language is set based on the user language settings.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_USER', 'locale-user');
/**
* The language is set based on the request/session parameters.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_SESSION', 'locale-session');
/**
* Regular expression pattern used to localize JavaScript strings.
*/
define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+');
/**
* Regular expression pattern used to match simple JS object literal.
*
* This pattern matches a basic JS object, but will fail on an object with
* nested objects. Used in JS file parsing for string arg processing.
*/
define('LOCALE_JS_OBJECT', '\{.*?\}');
/**
* Regular expression to match an object containing a key 'context'.
*
* Pattern to match a JS object containing a 'context key' with a string value,
* which is captured. Will fail if there are nested objects.
*/
define('LOCALE_JS_OBJECT_CONTEXT', '
\{ # match object literal start
.*? # match anything, non-greedy
(?: # match a form of "context"
\'context\'
|
"context"
|
context
)
\s*:\s* # match key-value separator ":"
(' . LOCALE_JS_STRING . ') # match context string
.*? # match anything, non-greedy
\} # match end of object literal
');
/**
* Translation import mode overwriting all existing translations
* if new translated version available.
*/
define('LOCALE_IMPORT_OVERWRITE', 0);
/**
* Translation import mode keeping existing translations and only
* inserting new strings.
*/
define('LOCALE_IMPORT_KEEP', 1);
/**
* URL language negotiation: use the path prefix as URL language
* indicator.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX', 0);
/**
* URL language negotiation: use the domain as URL language
* indicator.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN', 1);
/**
* @defgroup locale-languages-negotiation Language negotiation options
* @{
* Functions for language negotiation.
*
* There are functions that provide the ability to identify the
* language. This behavior can be controlled by various options.
*/
/**
* Identifies the language from the current interface language.
*
* @return
* The current interface language code.
*/
function locale_language_from_interface() {
global $language;
return isset($language->language) ? $language->language : FALSE;
}
/**
* Identify language from the Accept-language HTTP header we got.
*
* We perform browser accept-language parsing only if page cache is disabled,
* otherwise we would cache a user-specific preference.
*
* @param $languages
* An array of language objects for enabled languages ordered by weight.
*
* @return
* A valid language code on success, FALSE otherwise.
*/
function locale_language_from_browser($languages) {
if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
return FALSE;
}
// The Accept-Language header contains information about the language
// preferences configured in the user's browser / operating system.
// RFC 2616 (section 14.4) defines the Accept-Language header as follows:
// Accept-Language = "Accept-Language" ":"
// 1#( language-range [ ";" "q" "=" qvalue ] )
// language-range = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
// Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
$browser_langcodes = array();
if (preg_match_all('@([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
// We can safely use strtolower() here, tags are ASCII.
// RFC2616 mandates that the decimal part is no more than three digits,
// so we multiply the qvalue by 1000 to avoid floating point comparisons.
$langcode = strtolower($match[1]);
$qvalue = isset($match[2]) ? (float) $match[2] : 1;
$browser_langcodes[$langcode] = (int) ($qvalue * 1000);
}
}
// We should take pristine values from the HTTP headers, but Internet Explorer
// from version 7 sends only specific language tags (eg. fr-CA) without the
// corresponding generic tag (fr) unless explicitly configured. In that case,
// we assume that the lowest value of the specific tags is the value of the
// generic language to be as close to the HTTP 1.1 spec as possible.
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 and
// http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
asort($browser_langcodes);
foreach ($browser_langcodes as $langcode => $qvalue) {
$generic_tag = strtok($langcode, '-');
if (!isset($browser_langcodes[$generic_tag])) {
$browser_langcodes[$generic_tag] = $qvalue;
}
}
// Find the enabled language with the greatest qvalue, following the rules
// of RFC 2616 (section 14.4). If several languages have the same qvalue,
// prefer the one with the greatest weight.
$best_match_langcode = FALSE;
$max_qvalue = 0;
foreach ($languages as $langcode => $language) {
// Language tags are case insensitive (RFC2616, sec 3.10).
$langcode = strtolower($langcode);
// If nothing matches below, the default qvalue is the one of the wildcard
// language, if set, or is 0 (which will never match).
$qvalue = isset($browser_langcodes['*']) ? $browser_langcodes['*'] : 0;
// Find the longest possible prefix of the browser-supplied language
// ('the language-range') that matches this site language ('the language tag').
$prefix = $langcode;
do {
if (isset($browser_langcodes[$prefix])) {
$qvalue = $browser_langcodes[$prefix];
break;
}
}
while ($prefix = substr($prefix, 0, strrpos($prefix, '-')));
// Find the best match.
if ($qvalue > $max_qvalue) {
$best_match_langcode = $language->language;
$max_qvalue = $qvalue;
}
}
return $best_match_langcode;
}
/**
* Identify language from the user preferences.
*
* @param $languages
* An array of valid language objects.
*
* @return
* A valid language code on success, FALSE otherwise.
*/
function locale_language_from_user($languages) {
// User preference (only for logged users).
global $user;
if ($user->uid) {
return $user->language;
}
// No language preference from the user.
return FALSE;
}
/**
* Identify language from a request/session parameter.
*
* @param $languages
* An array of valid language objects.
*
* @return
* A valid language code on success, FALSE otherwise.
*/
function locale_language_from_session($languages) {
$param = variable_get('locale_language_negotiation_session_param', 'language');
// Request parameter: we need to update the session parameter only if we have
// an authenticated user.
if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) {
global $user;
if ($user->uid) {
$_SESSION[$param] = $langcode;
}
return $langcode;
}
// Session parameter.
if (isset($_SESSION[$param])) {
return $_SESSION[$param];
}
return FALSE;
}
/**
* Identify language via URL prefix or domain.
*
* @param $languages
* An array of valid language objects.
*
* @return
* A valid language code on success, FALSE otherwise.
*/
function locale_language_from_url($languages) {
$language_url = FALSE;
if (!language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) {
return $language_url;
}
switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
// $_GET['q'] might not be available at this time, because
// path initialization runs after the language bootstrap phase.
list($language, $_GET['q']) = language_url_split_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages);
if ($language !== FALSE) {
$language_url = $language->language;
}
break;
case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
foreach ($languages as $language) {
// Skip check if the language doesn't have a domain.
if ($language->domain) {
// Only compare the domains not the protocols or ports.
// Remove protocol and add http:// so parse_url works
$host = 'http://' . str_replace(array('http://', 'https://'), '', $language->domain);
$host = parse_url($host, PHP_URL_HOST);
if ($_SERVER['HTTP_HOST'] == $host) {
$language_url = $language->language;
break;
}
}
}
break;
}
return $language_url;
}
/**
* Determines the language to be assigned to URLs when none is detected.
*
* The language negotiation process has a fallback chain that ends with the
* default language provider. Each built-in language type has a separate
* initialization:
* - Interface language, which is the only configurable one, always gets a valid
* value. If no request-specific language is detected, the default language
* will be used.
* - Content language merely inherits the interface language by default.
* - URL language is detected from the requested URL and will be used to rewrite
* URLs appearing in the page being rendered. If no language can be detected,
* there are two possibilities:
* - If the default language has no configured path prefix or domain, then the
* default language is used. This guarantees that (missing) URL prefixes are
* preserved when navigating through the site.
* - If the default language has a configured path prefix or domain, a
* requested URL having an empty prefix or domain is an anomaly that must be
* fixed. This is done by introducing a prefix or domain in the rendered
* page matching the detected interface language.
*
* @param $languages
* (optional) An array of valid language objects. This is passed by
* language_provider_invoke() to every language provider callback, but it is
* not actually needed here. Defaults to NULL.
* @param $language_type
* (optional) The language type to fall back to. Defaults to the interface
* language.
*
* @return
* A valid language code.
*/
function locale_language_url_fallback($language = NULL, $language_type = LANGUAGE_TYPE_INTERFACE) {
$default = language_default();
$prefix = (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX);
// If the default language is not configured to convey language information,
// a missing URL language information indicates that URL language should be
// the default one, otherwise we fall back to an already detected language.
if (($prefix && empty($default->prefix)) || (!$prefix && empty($default->domain))) {
return $default->language;
}
else {
return $GLOBALS[$language_type]->language;
}
}
/**
* Return the URL language switcher block. Translation links may be provided by
* other modules.
*/
function locale_language_switcher_url($type, $path) {
$languages = language_list('enabled');
$links = array();
foreach ($languages[1] as $language) {
$links[$language->language] = array(
'href' => $path,
'title' => $language->native,
'language' => $language,
'attributes' => array('class' => array('language-link')),
);
}
return $links;
}
/**
* Return the session language switcher block.
*/
function locale_language_switcher_session($type, $path) {
drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
$param = variable_get('locale_language_negotiation_session_param', 'language');
$language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $GLOBALS[$type]->language;
$languages = language_list('enabled');
$links = array();
$query = $_GET;
unset($query['q']);
foreach ($languages[1] as $language) {
$langcode = $language->language;
$links[$langcode] = array(
'href' => $path,
'title' => $language->native,
'attributes' => array('class' => array('language-link')),
'query' => $query,
);
if ($language_query != $langcode) {
$links[$langcode]['query'][$param] = $langcode;
}
else {
$links[$langcode]['attributes']['class'][] = ' session-active';
}
}
return $links;
}
/**
* Rewrite URLs for the URL language provider.
*/
function locale_language_url_rewrite_url(&$path, &$options) {
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['languages'] = &drupal_static(__FUNCTION__);
}
$languages = &$drupal_static_fast['languages'];
if (!isset($languages)) {
$languages = language_list('enabled');
$languages = array_flip(array_keys($languages[1]));
}
// Language can be passed as an option, or we go for current URL language.
if (!isset($options['language'])) {
global $language_url;
$options['language'] = $language_url;
}
// We allow only enabled languages here.
elseif (!isset($languages[$options['language']->language])) {
unset($options['language']);
return;
}
if (isset($options['language'])) {
switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
if ($options['language']->domain) {
// Ask for an absolute URL with our modified base_url.
$options['absolute'] = TRUE;
$options['base_url'] = $options['language']->domain;
}
break;
case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
if (!empty($options['language']->prefix)) {
$options['prefix'] = $options['language']->prefix . '/';
}
break;
}
}
}
/**
* Rewrite URLs for the Session language provider.
*/
function locale_language_url_rewrite_session(&$path, &$options) {
static $query_rewrite, $query_param, $query_value;
// The following values are not supposed to change during a single page
// request processing.
if (!isset($query_rewrite)) {
global $user;
if (!$user->uid) {
$languages = language_list('enabled');
$languages = $languages[1];
$query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language'));
$query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL;
$query_rewrite = isset($languages[$query_value]) && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION);
}
else {
$query_rewrite = FALSE;
}
}
// If the user is anonymous, the user language provider is enabled, and the
// corresponding option has been set, we must preserve any explicit user
// language preference even with cookies disabled.
if ($query_rewrite) {
if (is_string($options['query'])) {
$options['query'] = drupal_get_query_array($options['query']);
}
if (!isset($options['query'][$query_param])) {
$options['query'][$query_param] = $query_value;
}
}
}
/**
* @} End of "locale-languages-negotiation"
*/
/**
* Check that a string is safe to be added or imported as a translation.
*
* This test can be used to detect possibly bad translation strings. It should
* not have any false positives. But it is only a test, not a transformation,
* as it destroys valid HTML. We cannot reliably filter translation strings
* on import because some strings are irreversibly corrupted. For example,
* a & in the translation would get encoded to &amp; by filter_xss()
* before being put in the database, and thus would be displayed incorrectly.
*
* The allowed tag list is like filter_xss_admin(), but omitting div and img as
* not needed for translation and likely to cause layout issues (div) or a
* possible attack vector (img).
*/
function locale_string_is_safe($string) {
return decode_entities($string) == decode_entities(filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var')));
}
/**
* @defgroup locale-api-add Language addition API
* @{
* Add a language.
*
* The language addition API is used to create languages and store them.
*/
/**
* API function to add a language.
*
* @param $langcode
* Language code.
* @param $name
* English name of the language
* @param $native
* Native name of the language
* @param $direction
* LANGUAGE_LTR or LANGUAGE_RTL
* @param $domain
* Optional custom domain name with protocol, without
* trailing slash (eg. http://de.example.com).
* @param $prefix
* Optional path prefix for the language. Defaults to the
* language code if omitted.
* @param $enabled
* Optionally TRUE to enable the language when created or FALSE to disable.
* @param $default
* Optionally set this language to be the default.
*/
function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) {
// Default prefix on language code.
if (empty($prefix)) {
$prefix = $langcode;
}
// If name was not set, we add a predefined language.
if (!isset($name)) {
include_once DRUPAL_ROOT . '/includes/iso.inc';
$predefined = _locale_get_predefined_list();
$name = $predefined[$langcode][0];
$native = isset($predefined[$langcode][1]) ? $predefined[$langcode][1] : $predefined[$langcode][0];
$direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR;
}
db_insert('languages')
->fields(array(
'language' => $langcode,
'name' => $name,
'native' => $native,
'direction' => $direction,
'domain' => $domain,
'prefix' => $prefix,
'enabled' => $enabled,
))
->execute();
// Only set it as default if enabled.
if ($enabled && $default) {
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' => ''));
}
if ($enabled) {
// Increment enabled language count if we are adding an enabled language.
variable_set('language_count', variable_get('language_count', 1) + 1);
}
// Kill the static cache in language_list().
drupal_static_reset('language_list');
// Force JavaScript translation file creation for the newly added language.
_locale_invalidate_js($langcode);
watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode));
module_invoke_all('multilingual_settings_changed');
}
/**
* @} End of "locale-api-add"
*/
/**
* @defgroup locale-api-import-export Translation import/export API.
* @{
* Functions to import and export translations.
*
* These functions provide the ability to import translations from
* external files and to export translations and translation templates.
*/
/**
* Parses Gettext Portable Object file information and inserts into database
*
* @param $file
* Drupal file object corresponding to the PO file to import.
* @param $langcode
* Language code.
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
* @param $group
* Text group to import PO file into (eg. 'default' for interface
* translations).
*/
function _locale_import_po($file, $langcode, $mode, $group = NULL) {
// Try to allocate enough time to parse and import the data.
drupal_set_time_limit(240);
// Check if we have the language already in the database.
if (!db_query("SELECT COUNT(language) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) {
drupal_set_message(t('The language selected for import is not supported.'), 'error');
return FALSE;
}
// Get strings from file (returns on failure after a partial import, or on success)
$status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group);
if ($status === FALSE) {
// Error messages are set in _locale_import_read_po().
return FALSE;
}
// Get status information on import process.
list($header_done, $additions, $updates, $deletes, $skips) = _locale_import_one_string('db-report');
if (!$header_done) {
drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error');
}
// Clear cache and force refresh of JavaScript translations.
_locale_invalidate_js($langcode);
cache_clear_all('locale:', 'cache', TRUE);
// Rebuild the menu, strings may have changed.
menu_rebuild();
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)));
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));
if ($skips) {
$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.');
drupal_set_message($skip_message);
watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING);
}
return TRUE;
}
/**
* Parses Gettext Portable Object file into an array
*
* @param $op
* Storage operation type: db-store or mem-store.
* @param $file
* Drupal file object corresponding to the PO file to import.
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
* @param $lang
* Language code.
* @param $group
* Text group to import PO file into (eg. 'default' for interface
* translations).
*/
function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
// The file will get closed by PHP on returning from this function.
$fd = fopen($file->uri, 'rb');
if (!$fd) {
_locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
return FALSE;
}
/*
* The parser context. Can be:
* - 'COMMENT' (#)
* - 'MSGID' (msgid)
* - 'MSGID_PLURAL' (msgid_plural)
* - 'MSGCTXT' (msgctxt)
* - 'MSGSTR' (msgstr or msgstr[])
* - 'MSGSTR_ARR' (msgstr_arg)
*/
$context = 'COMMENT';
// Current entry being read.
$current = array();
// Current plurality for 'msgstr[]'.
$plural = 0;
// Current line.
$lineno = 0;
while (!feof($fd)) {
// A line should not be longer than 10 * 1024.
$line = fgets($fd, 10 * 1024);
if ($lineno == 0) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace("\xEF\xBB\xBF", '', $line);
}
$lineno++;
// Trim away the linefeed.
$line = trim(strtr($line, array("\\\n" => "")));
if (!strncmp('#', $line, 1)) {
// Lines starting with '#' are comments.
if ($context == 'COMMENT') {
// Already in comment token, insert the comment.
$current['#'][] = substr($line, 1);
}
elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in string token, close it out.
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
// Start a new entry for the comment.
$current = array();
$current['#'][] = substr($line, 1);
$context = 'COMMENT';
}
else {
// A comment following any other token is a syntax error.
_locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
return FALSE;
}
}
elseif (!strncmp('msgid_plural', $line, 12)) {
// A plural form for the current message.
if ($context != 'MSGID') {
// A plural form cannot be added to anything else but the id directly.
_locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
return FALSE;
}
// Remove 'msgid_plural' and trim away whitespace.
$line = trim(substr($line, 12));
// At this point, $line should now contain only the plural form.
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
// The plural form must be wrapped in quotes.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
// Append the plural form to the current entry.
$current['msgid'] .= "\0" . $quoted;
$context = 'MSGID_PLURAL';
}
elseif (!strncmp('msgid', $line, 5)) {
// Starting a new message.
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in a message string, close it out.
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
// Start a new context for the id.
$current = array();
}
elseif ($context == 'MSGID') {
// We are currently already in the context, meaning we passed an id with no data.
_locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
// Remove 'msgid' and trim away whitespace.
$line = trim(substr($line, 5));
// At this point, $line should now contain only the message id.
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
// The message id must be wrapped in quotes.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current['msgid'] = $quoted;
$context = 'MSGID';
}
elseif (!strncmp('msgctxt', $line, 7)) {
// Starting a new context.
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in a message, start a new one.
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
$current = array();
}
elseif (!empty($current['msgctxt'])) {
// A context cannot apply to another context.
_locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
// Remove 'msgctxt' and trim away whitespaces.
$line = trim(substr($line, 7));
// At this point, $line should now contain the context.
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
// The context string must be quoted.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current['msgctxt'] = $quoted;
$context = 'MSGCTXT';
}
elseif (!strncmp('msgstr[', $line, 7)) {
// A message string for a specific plurality.
if (($context != 'MSGID') && ($context != 'MSGCTXT') && ($context != 'MSGID_PLURAL') && ($context != 'MSGSTR_ARR')) {
// Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries.
_locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
// Ensure the plurality is terminated.
if (strpos($line, ']') === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
// Extract the plurality.
$frombracket = strstr($line, '[');
$plural = substr($frombracket, 1, strpos($frombracket, ']') - 1);
// Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data.
$line = trim(strstr($line, " "));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current['msgstr'][$plural] = $quoted;
$context = 'MSGSTR_ARR';
}
elseif (!strncmp("msgstr", $line, 6)) {
// A string for the an id or context.
if (($context != 'MSGID') && ($context != 'MSGCTXT')) {
// Strings are only valid within an id or context scope.
_locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
// Remove 'msgstr' and trim away away whitespaces.
$line = trim(substr($line, 6));
// At this point, $line should now contain the message.
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current['msgstr'] = $quoted;
$context = 'MSGSTR';
}
elseif ($line != '') {
// Anything that is not a token may be a continuation of a previous token.
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
// Append the string to the current context.
if (($context == 'MSGID') || ($context == 'MSGID_PLURAL')) {
$current['msgid'] .= $quoted;
}
elseif ($context == 'MSGCTXT') {
$current['msgctxt'] .= $quoted;
}
elseif ($context == 'MSGSTR') {
$current['msgstr'] .= $quoted;
}
elseif ($context == 'MSGSTR_ARR') {
$current['msgstr'][$plural] .= $quoted;
}
else {
// No valid context to append to.
_locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno);
return FALSE;
}
}
}
// End of PO file, closed out the last entry.
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
}
elseif ($context != 'COMMENT') {
_locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
return FALSE;
}
}
/**
* Sets an error message occurred during locale file parsing.
*
* @param $message
* The message to be translated.
* @param $file
* Drupal file object corresponding to the PO file to import.
* @param $lineno
* An optional line number argument.
*/
function _locale_import_message($message, $file, $lineno = NULL) {
$vars = array('%filename' => $file->filename);
if (isset($lineno)) {
$vars['%line'] = $lineno;
}
$t = get_t();
drupal_set_message($t($message, $vars), 'error');
}
/**
* Imports a string into the database
*
* @param $op
* Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'.
* @param $value
* Details of the string stored.
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
* @param $lang
* Language to store the string in.
* @param $file
* Object representation of file being imported, only required when op is
* 'db-store'.
* @param $group
* Text group to import PO file into (eg. 'default' for interface
* translations).
*/
function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') {
$report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
$header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
$strings = &drupal_static(__FUNCTION__ . ':strings', array());
switch ($op) {
// Return stored strings
case 'mem-report':
return $strings;
// Store string in memory (only supports single strings)
case 'mem-store':
$strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr'];
return;
// Called at end of import to inform the user
case 'db-report':
return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']);
// Store the string we got in the database.
case 'db-store':
// We got header information.
if ($value['msgid'] == '') {
$languages = language_list();
if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) {
// Since we only need to parse the header if we ought to update the
// plural formula, only run this if we don't need to keep existing
// data untouched or if we don't have an existing plural formula.
$header = _locale_import_parse_header($value['msgstr']);
// Get the plural formula and update in database.
if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) {
list($nplurals, $plural) = $p;
db_update('languages')
->fields(array(
'plurals' => $nplurals,
'formula' => $plural,
))
->condition('language', $lang)
->execute();
}
else {
db_update('languages')
->fields(array(
'plurals' => 0,
'formula' => '',
))
->condition('language', $lang)
->execute();
}