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