Newer
Older
* Administration functions for locale.module.
Angie Byron
committed
/**
* The language is determined using a URL language indicator:
* path prefix or domain according to the configuration.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_URL', 'locale-url');
/**
* The language is set based on the browser language settings.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_BROWSER', 'locale-browser');
/**
* The language is determined using the current interface language.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_INTERFACE', 'locale-interface');
Dries Buytaert
committed
/**
* If no URL language is available language is determined using an already
* detected one.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_URL_FALLBACK', 'locale-url-fallback');
Angie Byron
committed
/**
* The language is set based on the user language settings.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_USER', 'locale-user');
/**
* The language is set based on the request/session parameters.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_SESSION', 'locale-session');
/**
* Regular expression pattern used to localize JavaScript strings.
*/
Gábor Hojtsy
committed
define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+');
Angie Byron
committed
/**
* Regular expression pattern used to match simple JS object literal.
*
* This pattern matches a basic JS object, but will fail on an object with
* nested objects. Used in JS file parsing for string arg processing.
*/
define('LOCALE_JS_OBJECT', '\{.*?\}');
/**
* Regular expression to match an object containing a key 'context'.
*
* Pattern to match a JS object containing a 'context key' with a string value,
* which is captured. Will fail if there are nested objects.
*/
define('LOCALE_JS_OBJECT_CONTEXT', '
\{ # match object literal start
.*? # match anything, non-greedy
(?: # match a form of "context"
\'context\'
|
"context"
|
context
)
\s*:\s* # match key-value separator ":"
(' . LOCALE_JS_STRING . ') # match context string
.*? # match anything, non-greedy
\} # match end of object literal
');
Gábor Hojtsy
committed
/**
* Translation import mode overwriting all existing translations
* if new translated version available.
*/
define('LOCALE_IMPORT_OVERWRITE', 0);
/**
* Translation import mode keeping existing translations and only
* inserting new strings.
*/
define('LOCALE_IMPORT_KEEP', 1);
Angie Byron
committed
/**
* URL language negotiation: use the path prefix as URL language
* indicator.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX', 0);
/**
* URL language negotiation: use the domain as URL language
* indicator.
*/
define('LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN', 1);
Angie Byron
committed
* @defgroup locale-languages-negotiation Language negotiation options
* Functions for language negotiation.
*
* There are functions that provide the ability to identify the
* language. This behavior can be controlled by various options.
Angie Byron
committed
/**
Angie Byron
committed
* Identifies the language from the current interface language.
Angie Byron
committed
*
* @return
Angie Byron
committed
* The current interface language code.
Angie Byron
committed
*/
Angie Byron
committed
function locale_language_from_interface() {
Angie Byron
committed
global $language;
return isset($language->language) ? $language->language : FALSE;
}
/**
* Identify language from the Accept-language HTTP header we got.
*
* We perform browser accept-language parsing only if page cache is disabled,
* otherwise we would cache a user-specific preference.
*
* @param $languages
Angie Byron
committed
* An array of language objects for enabled languages ordered by weight.
Angie Byron
committed
*
* @return
* A valid language code on success, FALSE otherwise.
*/
function locale_language_from_browser($languages) {
Angie Byron
committed
if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
return FALSE;
}
// The Accept-Language header contains information about the language
// preferences configured in the user's browser / operating system.
// RFC 2616 (section 14.4) defines the Accept-Language header as follows:
// Accept-Language = "Accept-Language" ":"
// 1#( language-range [ ";" "q" "=" qvalue ] )
// language-range = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )
Angie Byron
committed
// Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
Angie Byron
committed
$browser_langcodes = array();
if (preg_match_all('@([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
// We can safely use strtolower() here, tags are ASCII.
// RFC2616 mandates that the decimal part is no more than three digits,
// so we multiply the qvalue by 1000 to avoid floating point comparisons.
$langcode = strtolower($match[1]);
$qvalue = isset($match[2]) ? (float) $match[2] : 1;
$browser_langcodes[$langcode] = (int) ($qvalue * 1000);
}
}
// We should take pristine values from the HTTP headers, but Internet Explorer
// from version 7 sends only specific language tags (eg. fr-CA) without the
// corresponding generic tag (fr) unless explicitly configured. In that case,
// we assume that the lowest value of the specific tags is the value of the
// generic language to be as close to the HTTP 1.1 spec as possible.
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 and
// http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx
asort($browser_langcodes);
foreach ($browser_langcodes as $langcode => $qvalue) {
$generic_tag = strtok($langcode, '-');
if (!isset($browser_langcodes[$generic_tag])) {
$browser_langcodes[$generic_tag] = $qvalue;
Angie Byron
committed
}
}
Angie Byron
committed
// Find the enabled language with the greatest qvalue, following the rules
// of RFC 2616 (section 14.4). If several languages have the same qvalue,
// prefer the one with the greatest weight.
$best_match_langcode = FALSE;
$max_qvalue = 0;
foreach ($languages as $langcode => $language) {
// Language tags are case insensitive (RFC2616, sec 3.10).
$langcode = strtolower($langcode);
// If nothing matches below, the default qvalue is the one of the wildcard
// language, if set, or is 0 (which will never match).
$qvalue = isset($browser_langcodes['*']) ? $browser_langcodes['*'] : 0;
// Find the longest possible prefix of the browser-supplied language
// ('the language-range') that matches this site language ('the language tag').
$prefix = $langcode;
do {
if (isset($browser_langcodes[$prefix])) {
$qvalue = $browser_langcodes[$prefix];
break;
}
}
while ($prefix = substr($prefix, 0, strrpos($prefix, '-')));
Angie Byron
committed
Angie Byron
committed
// Find the best match.
if ($qvalue > $max_qvalue) {
$best_match_langcode = $language->language;
$max_qvalue = $qvalue;
Angie Byron
committed
}
}
Angie Byron
committed
return $best_match_langcode;
Angie Byron
committed
}
/**
* Identify language from the user preferences.
*
* @param $languages
* An array of valid language objects.
*
* @return
* A valid language code on success, FALSE otherwise.
Angie Byron
committed
*/
function locale_language_from_user($languages) {
// User preference (only for logged users).
global $user;
if ($user->uid) {
return $user->language;
}
// No language preference from the user.
return FALSE;
}
/**
* Identify language from a request/session parameter.
*
* @param $languages
* An array of valid language objects.
*
* @return
* A valid language code on success, FALSE otherwise.
Angie Byron
committed
*/
function locale_language_from_session($languages) {
$param = variable_get('locale_language_negotiation_session_param', 'language');
Dries Buytaert
committed
// Request parameter: we need to update the session parameter only if we have
// an authenticated user.
Angie Byron
committed
if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) {
Dries Buytaert
committed
global $user;
if ($user->uid) {
$_SESSION[$param] = $langcode;
}
return $langcode;
Angie Byron
committed
}
// Session parameter.
if (isset($_SESSION[$param])) {
return $_SESSION[$param];
}
return FALSE;
}
/**
* Identify language via URL prefix or domain.
*
* @param $languages
* An array of valid language objects.
*
* @return
* A valid language code on success, FALSE otherwise.
Angie Byron
committed
*/
function locale_language_from_url($languages) {
$language_url = FALSE;
Angie Byron
committed
if (!language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) {
return $language_url;
}
Angie Byron
committed
switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
// $_GET['q'] might not be available at this time, because
// path initialization runs after the language bootstrap phase.
list($language, $_GET['q']) = language_url_split_prefix(isset($_GET['q']) ? $_GET['q'] : NULL, $languages);
if ($language !== FALSE) {
$language_url = $language->language;
}
break;
case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
foreach ($languages as $language) {
Dries Buytaert
committed
// Skip check if the language doesn't have a domain.
if ($language->domain) {
// Only compare the domains not the protocols or ports.
// Remove protocol and add http:// so parse_url works
$host = 'http://' . str_replace(array('http://', 'https://'), '', $language->domain);
$host = parse_url($host, PHP_URL_HOST);
if ($_SERVER['HTTP_HOST'] == $host) {
$language_url = $language->language;
break;
}
Angie Byron
committed
}
}
break;
}
return $language_url;
}
Dries Buytaert
committed
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/**
* Determines the language to be assigned to URLs when none is detected.
*
* The language negotiation process has a fallback chain that ends with the
* default language provider. Each built-in language type has a separate
* initialization:
* - Interface language, which is the only configurable one, always gets a valid
* value. If no request-specific language is detected, the default language
* will be used.
* - Content language merely inherits the interface language by default.
* - URL language is detected from the requested URL and will be used to rewrite
* URLs appearing in the page being rendered. If no language can be detected,
* there are two possibilities:
* - If the default language has no configured path prefix or domain, then the
* default language is used. This guarantees that (missing) URL prefixes are
* preserved when navigating through the site.
* - If the default language has a configured path prefix or domain, a
* requested URL having an empty prefix or domain is an anomaly that must be
* fixed. This is done by introducing a prefix or domain in the rendered
* page matching the detected interface language.
*
* @param $languages
* (optional) An array of valid language objects. This is passed by
* language_provider_invoke() to every language provider callback, but it is
* not actually needed here. Defaults to NULL.
* @param $language_type
* (optional) The language type to fall back to. Defaults to the interface
* language.
*
* @return
* A valid language code.
*/
function locale_language_url_fallback($language = NULL, $language_type = LANGUAGE_TYPE_INTERFACE) {
$default = language_default();
$prefix = (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX);
// If the default language is not configured to convey language information,
// a missing URL language information indicates that URL language should be
// the default one, otherwise we fall back to an already detected language.
if (($prefix && empty($default->prefix)) || (!$prefix && empty($default->domain))) {
return $default->language;
}
else {
return $GLOBALS[$language_type]->language;
}
}
Angie Byron
committed
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
/**
* Return the URL language switcher block. Translation links may be provided by
* other modules.
*/
function locale_language_switcher_url($type, $path) {
$languages = language_list('enabled');
$links = array();
foreach ($languages[1] as $language) {
$links[$language->language] = array(
'href' => $path,
'title' => $language->native,
'language' => $language,
'attributes' => array('class' => array('language-link')),
);
}
return $links;
}
/**
* Return the session language switcher block.
*/
function locale_language_switcher_session($type, $path) {
drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
$param = variable_get('locale_language_negotiation_session_param', 'language');
$language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $GLOBALS[$type]->language;
$languages = language_list('enabled');
$links = array();
$query = $_GET;
unset($query['q']);
foreach ($languages[1] as $language) {
$langcode = $language->language;
$links[$langcode] = array(
'href' => $path,
'title' => $language->native,
'attributes' => array('class' => array('language-link')),
'query' => $query,
);
if ($language_query != $langcode) {
$links[$langcode]['query'][$param] = $langcode;
}
else {
$links[$langcode]['attributes']['class'][] = ' session-active';
}
}
return $links;
}
/**
* Rewrite URLs for the URL language provider.
*/
function locale_language_url_rewrite_url(&$path, &$options) {
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['languages'] = &drupal_static(__FUNCTION__);
}
$languages = &$drupal_static_fast['languages'];
if (!isset($languages)) {
$languages = language_list('enabled');
$languages = array_flip(array_keys($languages[1]));
}
Angie Byron
committed
// Language can be passed as an option, or we go for current URL language.
if (!isset($options['language'])) {
global $language_url;
$options['language'] = $language_url;
}
// We allow only enabled languages here.
elseif (!isset($languages[$options['language']->language])) {
unset($options['language']);
return;
}
Angie Byron
committed
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
if (isset($options['language'])) {
switch (variable_get('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX)) {
case LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN:
if ($options['language']->domain) {
// Ask for an absolute URL with our modified base_url.
$options['absolute'] = TRUE;
$options['base_url'] = $options['language']->domain;
}
break;
case LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX:
if (!empty($options['language']->prefix)) {
$options['prefix'] = $options['language']->prefix . '/';
}
break;
}
}
}
/**
* Rewrite URLs for the Session language provider.
*/
function locale_language_url_rewrite_session(&$path, &$options) {
static $query_rewrite, $query_param, $query_value;
// The following values are not supposed to change during a single page
// request processing.
if (!isset($query_rewrite)) {
global $user;
if (!$user->uid) {
$languages = language_list('enabled');
$languages = $languages[1];
$query_param = check_plain(variable_get('locale_language_negotiation_session_param', 'language'));
$query_value = isset($_GET[$query_param]) ? check_plain($_GET[$query_param]) : NULL;
$query_rewrite = isset($languages[$query_value]) && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_SESSION);
}
else {
$query_rewrite = FALSE;
}
}
// If the user is anonymous, the user language provider is enabled, and the
// corresponding option has been set, we must preserve any explicit user
// language preference even with cookies disabled.
if ($query_rewrite) {
if (is_string($options['query'])) {
$options['query'] = drupal_get_query_array($options['query']);
}
if (!isset($options['query'][$query_param])) {
$options['query'][$query_param] = $query_value;
}
}
}
/**
* @} End of "locale-languages-negotiation"
*/
Dries Buytaert
committed
/**
* Check that a string is safe to be added or imported as a translation.
*
* This test can be used to detect possibly bad translation strings. It should
* not have any false positives. But it is only a test, not a transformation,
* as it destroys valid HTML. We cannot reliably filter translation strings
* on import because some strings are irreversibly corrupted. For example,
Dries Buytaert
committed
* a & in the translation would get encoded to & by filter_xss()
* before being put in the database, and thus would be displayed incorrectly.
*
* The allowed tag list is like filter_xss_admin(), but omitting div and img as
* not needed for translation and likely to cause layout issues (div) or a
* possible attack vector (img).
*/
function locale_string_is_safe($string) {
return decode_entities($string) == decode_entities(filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var')));
}
* @defgroup locale-api-add Language addition API
* Add a language.
*
* The language addition API is used to create languages and store them.
*/
/**
* API function to add a language.
*
* @param $langcode
* Language code.
* @param $name
* English name of the language
* @param $native
* Native name of the language
* @param $direction
* LANGUAGE_LTR or LANGUAGE_RTL
* @param $domain
* Optional custom domain name with protocol, without
* trailing slash (eg. http://de.example.com).
* @param $prefix
* Optional path prefix for the language. Defaults to the
* language code if omitted.
Dries Buytaert
committed
* @param $enabled
* Optionally TRUE to enable the language when created or FALSE to disable.
* @param $default
* Optionally set this language to be the default.
Dries Buytaert
committed
function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) {
// Default prefix on language code.
if (empty($prefix)) {
$prefix = $langcode;
Dries Buytaert
committed
// If name was not set, we add a predefined language.
if (!isset($name)) {
include_once DRUPAL_ROOT . '/includes/iso.inc';
Dries Buytaert
committed
$predefined = _locale_get_predefined_list();
$name = $predefined[$langcode][0];
$native = isset($predefined[$langcode][1]) ? $predefined[$langcode][1] : $predefined[$langcode][0];
$direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR;
Dries Buytaert
committed
}
Dries Buytaert
committed
db_insert('languages')
->fields(array(
'language' => $langcode,
'name' => $name,
'native' => $native,
'direction' => $direction,
'domain' => $domain,
'prefix' => $prefix,
'enabled' => $enabled,
))
->execute();
Dries Buytaert
committed
// Only set it as default if enabled.
if ($enabled && $default) {
Gábor Hojtsy
committed
variable_set('language_default', (object) array('language' => $langcode, 'name' => $name, 'native' => $native, 'direction' => $direction, 'enabled' => (int) $enabled, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => $prefix, 'weight' => 0, 'javascript' => ''));
if ($enabled) {
// Increment enabled language count if we are adding an enabled language.
variable_set('language_count', variable_get('language_count', 1) + 1);
}
Dries Buytaert
committed
// Kill the static cache in language_list().
drupal_static_reset('language_list');
Gábor Hojtsy
committed
// Force JavaScript translation file creation for the newly added language.
_locale_invalidate_js($langcode);
watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode));
Dries Buytaert
committed
module_invoke_all('multilingual_settings_changed');
/**
* @} End of "locale-api-add"
*/
* @defgroup locale-api-import-export Translation import/export API.
* Functions to import and export translations.
*
* These functions provide the ability to import translations from
* external files and to export translations and translation templates.
/**
* Parses Gettext Portable Object file information and inserts into database
*
Dries Buytaert
committed
* Drupal file object corresponding to the PO file to import.
Gábor Hojtsy
committed
* @param $langcode
Dries Buytaert
committed
* Language code.
Dries Buytaert
committed
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
Dries Buytaert
committed
* Text group to import PO file into (eg. 'default' for interface
* translations).
Gábor Hojtsy
committed
function _locale_import_po($file, $langcode, $mode, $group = NULL) {
// Try to allocate enough time to parse and import the data.
drupal_set_time_limit(240);
Gábor Hojtsy
committed
// Check if we have the language already in the database.
Dries Buytaert
committed
if (!db_query("SELECT COUNT(language) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) {
drupal_set_message(t('The language selected for import is not supported.'), 'error');
// Get strings from file (returns on failure after a partial import, or on success)
Gábor Hojtsy
committed
$status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group);
if ($status === FALSE) {
Gábor Hojtsy
committed
// Error messages are set in _locale_import_read_po().
return FALSE;
}
Gábor Hojtsy
committed
// Get status information on import process.
Dries Buytaert
committed
list($header_done, $additions, $updates, $deletes, $skips) = _locale_import_one_string('db-report');
Dries Buytaert
committed
if (!$header_done) {
drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error');
Gábor Hojtsy
committed
// Clear cache and force refresh of JavaScript translations.
_locale_invalidate_js($langcode);
cache_clear_all('locale:', 'cache', TRUE);
Gábor Hojtsy
committed
// Rebuild the menu, strings may have changed.
Gábor Hojtsy
committed
drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)));
watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes));
Dries Buytaert
committed
if ($skips) {
$skip_message = format_plural($skips, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.');
drupal_set_message($skip_message);
watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING);
Dries Buytaert
committed
}
return TRUE;
}
/**
* Parses Gettext Portable Object file into an array
*
Dries Buytaert
committed
* Storage operation type: db-store or mem-store.
Dries Buytaert
committed
* Drupal file object corresponding to the PO file to import.
Dries Buytaert
committed
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
Dries Buytaert
committed
* Language code.
Dries Buytaert
committed
* Text group to import PO file into (eg. 'default' for interface
* translations).
Gábor Hojtsy
committed
function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
// The file will get closed by PHP on returning from this function.
$fd = fopen($file->uri, 'rb');
_locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
/*
* The parser context. Can be:
* - 'COMMENT' (#)
* - 'MSGID' (msgid)
* - 'MSGID_PLURAL' (msgid_plural)
* - 'MSGCTXT' (msgctxt)
* - 'MSGSTR' (msgstr or msgstr[])
* - 'MSGSTR_ARR' (msgstr_arg)
*/
$context = 'COMMENT';
// Current entry being read.
$current = array();
// Current plurality for 'msgstr[]'.
$plural = 0;
// Current line.
$lineno = 0;
while (!feof($fd)) {
// A line should not be longer than 10 * 1024.
$line = fgets($fd, 10 * 1024);
if ($lineno == 0) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace("\xEF\xBB\xBF", '', $line);
}
// Trim away the linefeed.
$line = trim(strtr($line, array("\\\n" => "")));
if (!strncmp('#', $line, 1)) {
// Lines starting with '#' are comments.
if ($context == 'COMMENT') {
// Already in comment token, insert the comment.
$current['#'][] = substr($line, 1);
elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in string token, close it out.
Gábor Hojtsy
committed
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
// Start a new entry for the comment.
$current = array();
$current['#'][] = substr($line, 1);
$context = 'COMMENT';
else {
// A comment following any other token is a syntax error.
_locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
elseif (!strncmp('msgid_plural', $line, 12)) {
// A plural form for the current message.
if ($context != 'MSGID') {
// A plural form cannot be added to anything else but the id directly.
_locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
// Remove 'msgid_plural' and trim away whitespace.
// At this point, $line should now contain only the plural form.
if ($quoted === FALSE) {
// The plural form must be wrapped in quotes.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
// Append the plural form to the current entry.
$current['msgid'] .= "\0" . $quoted;
$context = 'MSGID_PLURAL';
elseif (!strncmp('msgid', $line, 5)) {
// Starting a new message.
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in a message string, close it out.
Gábor Hojtsy
committed
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
// Start a new context for the id.
elseif ($context == 'MSGID') {
// We are currently already in the context, meaning we passed an id with no data.
_locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno);
// Remove 'msgid' and trim away whitespace.
// At this point, $line should now contain only the message id.
if ($quoted === FALSE) {
// The message id must be wrapped in quotes.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
$current['msgid'] = $quoted;
$context = 'MSGID';
elseif (!strncmp('msgctxt', $line, 7)) {
// Starting a new context.
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in a message, start a new one.
Dries Buytaert
committed
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
$current = array();
}
elseif (!empty($current['msgctxt'])) {
// A context cannot apply to another context.
_locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno);
Dries Buytaert
committed
return FALSE;
}
// Remove 'msgctxt' and trim away whitespaces.
Dries Buytaert
committed
$line = trim(substr($line, 7));
// At this point, $line should now contain the context.
Dries Buytaert
committed
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
// The context string must be quoted.
Dries Buytaert
committed
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current['msgctxt'] = $quoted;
$context = 'MSGCTXT';
Dries Buytaert
committed
}
elseif (!strncmp('msgstr[', $line, 7)) {
// A message string for a specific plurality.
if (($context != 'MSGID') && ($context != 'MSGCTXT') && ($context != 'MSGID_PLURAL') && ($context != 'MSGSTR_ARR')) {
// Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries.
_locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno);
// Ensure the plurality is terminated.
if (strpos($line, ']') === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
// Extract the plurality.
$frombracket = strstr($line, '[');
$plural = substr($frombracket, 1, strpos($frombracket, ']') - 1);
// Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data.
if ($quoted === FALSE) {
// The string must be quoted.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
$current['msgstr'][$plural] = $quoted;
$context = 'MSGSTR_ARR';
// A string for the an id or context.
if (($context != 'MSGID') && ($context != 'MSGCTXT')) {
// Strings are only valid within an id or context scope.
_locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno);
// Remove 'msgstr' and trim away away whitespaces.
// At this point, $line should now contain the message.
if ($quoted === FALSE) {
// The string must be quoted.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
$current['msgstr'] = $quoted;
$context = 'MSGSTR';
elseif ($line != '') {
// Anything that is not a token may be a continuation of a previous token.
if ($quoted === FALSE) {
// The string must be quoted.
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
// Append the string to the current context.
if (($context == 'MSGID') || ($context == 'MSGID_PLURAL')) {
$current['msgid'] .= $quoted;
elseif ($context == 'MSGCTXT') {
$current['msgctxt'] .= $quoted;
Dries Buytaert
committed
}
elseif ($context == 'MSGSTR') {
$current['msgstr'] .= $quoted;
elseif ($context == 'MSGSTR_ARR') {
$current['msgstr'][$plural] .= $quoted;
// No valid context to append to.
_locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno);
// End of PO file, closed out the last entry.
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
Gábor Hojtsy
committed
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
elseif ($context != 'COMMENT') {
_locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
* Sets an error message occurred during locale file parsing.
Dries Buytaert
committed
* The message to be translated.
Dries Buytaert
committed
* Drupal file object corresponding to the PO file to import.
Dries Buytaert
committed
* An optional line number argument.
*/
function _locale_import_message($message, $file, $lineno = NULL) {
$vars = array('%filename' => $file->filename);
if (isset($lineno)) {
$vars['%line'] = $lineno;
}
drupal_set_message($t($message, $vars), 'error');
}
/**
* Imports a string into the database
*
Dries Buytaert
committed
* Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'.
Dries Buytaert
committed
* Details of the string stored.
Dries Buytaert
committed
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
Dries Buytaert
committed
* Language to store the string in.
* @param $file
Dries Buytaert
committed
* Object representation of file being imported, only required when op is
* 'db-store'.
Dries Buytaert
committed
* Text group to import PO file into (eg. 'default' for interface
* translations).
function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') {
Dries Buytaert
committed
$report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
$header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
$strings = &drupal_static(__FUNCTION__ . ':strings', array());
switch ($op) {
// Return stored strings
case 'mem-report':
return $strings;
// Store string in memory (only supports single strings)
case 'mem-store':
Dries Buytaert
committed
$strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr'];
return;
// Called at end of import to inform the user
case 'db-report':
Dries Buytaert
committed
return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']);
Gábor Hojtsy
committed
// Store the string we got in the database.
Gábor Hojtsy
committed
// We got header information.
Dries Buytaert
committed
$languages = language_list();
if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) {
// Since we only need to parse the header if we ought to update the
Dries Buytaert
committed
// plural formula, only run this if we don't need to keep existing
Dries Buytaert
committed
// data untouched or if we don't have an existing plural formula.
$header = _locale_import_parse_header($value['msgstr']);
// Get the plural formula and update in database.
if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) {
list($nplurals, $plural) = $p;
db_update('languages')
->fields(array(
'plurals' => $nplurals,
'formula' => $plural,
))
->condition('language', $lang)
->execute();
}
else {
db_update('languages')
->fields(array(
'plurals' => 0,
'formula' => '',
))
->condition('language', $lang)
->execute();
}