Newer
Older
* Administration functions for locale.module.
/**
* Regular expression pattern used to localize JavaScript strings.
*/
Gábor Hojtsy
committed
define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+');
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
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
global $language;
return isset($language->language) ? $language->language : FALSE;
}
/**
* Identify language from the Accept-language HTTP header we got.
*
* We perform browser accept-language parsing only if page cache is disabled,
* otherwise we would cache a user-specific preference.
*
* @param $languages
* An array of valid language objects.
*
* @return
* A valid language code on success, FALSE otherwise.
*/
function locale_language_from_browser($languages) {
// Specified by the user via the browser's Accept Language setting
// Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5"
$browser_langs = array();
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$browser_accept = explode(",", $_SERVER['HTTP_ACCEPT_LANGUAGE']);
foreach ($browser_accept as $langpart) {
// The language part is either a code or a code with a quality.
// We cannot do anything with a * code, so it is skipped.
// If the quality is missing, it is assumed to be 1 according to the RFC.
if (preg_match("!([a-z-]+)(;q=([0-9\\.]+))?!", trim($langpart), $found)) {
$browser_langs[$found[1]] = (isset($found[3]) ? (float) $found[3] : 1.0);
}
}
}
// Order the codes by quality
arsort($browser_langs);
// Try to find the first preferred language we have
foreach ($browser_langs as $langcode => $q) {
if (isset($languages[$langcode])) {
return $langcode;
}
}
return FALSE;
}
/**
* 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');
// Request parameter.
if (isset($_GET[$param]) && isset($languages[$langcode = $_GET[$param]])) {
return $_SESSION[$param] = $langcode;
}
// Session parameter.
if (isset($_SESSION[$param])) {
return $_SESSION[$param];
}
return FALSE;
}
/**
* Identify language via URL prefix or domain.
*
* @param $languages
* An array of valid language objects.
*
* @return
* A valid language code on success, FALSE otherwise.
Angie Byron
committed
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
*/
function locale_language_from_url($languages) {
$language_url = FALSE;
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) {
$host = parse_url($language->domain, PHP_URL_HOST);
if ($host && ($_SERVER['HTTP_HOST'] == $host)) {
$language_url = $language->language;
break;
}
}
break;
}
return $language_url;
}
/**
* 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) {
// 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;
}
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.
* @{
*/
/**
* 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 Translation import API.
* @{
*/
/**
* Parses Gettext Portable Object file information and inserts into database
*
* @param $file
* Drupal file object corresponding to the PO file to import
Gábor Hojtsy
committed
* @param $langcode
Gábor Hojtsy
committed
* Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
* @param $group
* Text group to import PO file into (eg. 'default' for interface translations)
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
*
* @param $op
* Storage operation type: db-store or mem-store
* @param $file
* Drupal file object corresponding to the PO file to import
* @param $mode
Gábor Hojtsy
committed
* Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
* @param $group
* Text group to import PO file into (eg. 'default' for interface translations)
Gábor Hojtsy
committed
function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
$fd = fopen($file->uri, "rb"); // File will get closed by PHP on return
_locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
return FALSE;
}
$context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR
$current = array(); // Current entry being read
$plural = 0; // Current plural form
$lineno = 0; // Current line
while (!feof($fd)) {
$line = fgets($fd, 10*1024); // A line should not be this long
if ($lineno == 0) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace("\xEF\xBB\xBF", '', $line);
}
$line = trim(strtr($line, array("\\\n" => "")));
if (!strncmp("#", $line, 1)) { // A comment
if ($context == "COMMENT") { // Already in comment context: add
$current["#"][] = substr($line, 1);
}
elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
Gábor Hojtsy
committed
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
$current = array();
$current["#"][] = substr($line, 1);
$context = "COMMENT";
}
else { // Parse error
_locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
return FALSE;
}
}
elseif (!strncmp("msgid_plural", $line, 12)) {
if ($context != "MSGID") { // Must be plural form for current entry
_locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
return FALSE;
}
$line = trim(substr($line, 12));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
$current["msgid"] = $current["msgid"] . "\0" . $quoted;
}
elseif (!strncmp("msgid", $line, 5)) {
if ($context == "MSGSTR") { // End current entry, start a new one
Gábor Hojtsy
committed
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
$current = array();
}
elseif ($context == "MSGID") { // Already in this context? Parse error
_locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
$line = trim(substr($line, 5));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current["msgid"] = $quoted;
$context = "MSGID";
}
Dries Buytaert
committed
elseif (!strncmp("msgctxt", $line, 7)) {
if ($context == "MSGSTR") { // End current entry, start a new one
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
$current = array();
}
elseif (!empty($current["msgctxt"])) { // Already in this context? Parse error
_locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno);
Dries Buytaert
committed
return FALSE;
}
$line = trim(substr($line, 7));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_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
if (($context != "MSGID") && ($context != "MSGCTXT") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) { // Must come after msgid, msgxtxt, msgid_plural, or msgstr[]
_locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno);
if (strpos($line, "]") === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$frombracket = strstr($line, "[");
$plural = substr($frombracket, 1, strpos($frombracket, "]") - 1);
$line = trim(strstr($line, " "));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current["msgstr"][$plural] = $quoted;
$context = "MSGSTR_ARR";
}
elseif (!strncmp("msgstr", $line, 6)) {
Dries Buytaert
committed
if (($context != "MSGID") && ($context != "MSGCTXT")) { // Should come just after a msgid or msgctxt block
_locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno);
return FALSE;
}
$line = trim(substr($line, 6));
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
$current["msgstr"] = $quoted;
$context = "MSGSTR";
}
elseif ($line != "") {
$quoted = _locale_import_parse_quoted($line);
if ($quoted === FALSE) {
_locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
return FALSE;
}
if (($context == "MSGID") || ($context == "MSGID_PLURAL")) {
$current["msgid"] .= $quoted;
}
Dries Buytaert
committed
elseif ($context == "MSGCTXT") {
$current["msgctxt"] .= $quoted;
}
elseif ($context == "MSGSTR") {
$current["msgstr"] .= $quoted;
}
elseif ($context == "MSGSTR_ARR") {
$current["msgstr"][$plural] .= $quoted;
}
else {
_locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno);
return FALSE;
}
}
}
// End of PO file, flush last entry
Dries Buytaert
committed
if (!empty($current) && !empty($current['msgstr'])) {
Gábor Hojtsy
committed
_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
_locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
* Sets an error message occurred during locale file parsing.
*
* @param $message
* The message to be translated
* @param $file
* Drupal file object corresponding to the PO file to import
* @param $lineno
* An optional line number argument
*/
function _locale_import_message($message, $file, $lineno = NULL) {
$vars = array('%filename' => $file->filename);
if (isset($lineno)) {
$vars['%line'] = $lineno;
}
drupal_set_message($t($message, $vars), 'error');
}
/**
* Imports a string into the database
*
* @param $op
* Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'
* @param $value
* Details of the string stored
* @param $mode
Gábor Hojtsy
committed
* Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
* @param $lang
* Language to store the string in
* @param $file
* Object representation of file being imported, only required when op is 'db-store'
* @param $group
* Text group to import PO file into (eg. 'default' for interface translations)
function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') {
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();
}
Dries Buytaert
committed
$header_done = TRUE;
Gábor Hojtsy
committed
// Some real string to import.
$comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']);
Gábor Hojtsy
committed
// This string has plural versions.
$english = explode("\0", $value['msgid'], 2);
$entries = array_keys($value['msgstr']);
for ($i = 3; $i <= count($entries); $i++) {
$english[] = $english[1];
}
$translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries);
$english = array_map('_locale_import_append_plural', $english, $entries);
foreach ($translation as $key => $trans) {
if ($key == 0) {
$plid = 0;
Dries Buytaert
committed
$plid = _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $group, $comments, $mode, $plid, $key);
Gábor Hojtsy
committed
// A simple string to import.
$english = $value['msgid'];
$translation = $value['msgstr'];
Dries Buytaert
committed
_locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $group, $comments, $mode);
Gábor Hojtsy
committed
/**
* Import one string into the database.
*
* @param $report
* Report array summarizing the number of changes done in the form:
* array(inserts, updates, deletes).
* @param $langcode
* Language code to import string into.
Dries Buytaert
committed
* @param $context
* The context of this string.
Gábor Hojtsy
committed
* @param $source
* Source string.
* @param $translation
* Translation to language specified in $langcode.
* @param $textgroup
* Name of textgroup to store translation in.
* @param $location
* Location value to save with source string.
* @param $mode
* Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
* @param $plid
* Optional plural ID to use.
* @param $plural
* Optional plural value to use.
* @return
* The string ID of the existing string modified or the new string added.
*/
Dries Buytaert
committed
function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $plid = 0, $plural = 0) {
$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();
Gábor Hojtsy
committed
if (!empty($translation)) {
Dries Buytaert
committed
// Skip this string unless it passes a check for dangerous code.
Angie Byron
committed
// Text groups other than default still can contain HTML tags
// (i.e. translatable blocks).
if ($textgroup == "default" && !locale_string_is_safe($translation)) {
Dries Buytaert
committed
$report['skips']++;
$lid = 0;
}
elseif ($lid) {
Gábor Hojtsy
committed
// We have this source string saved already.
Dries Buytaert
committed
db_update('locales_source')
->fields(array(
'location' => $location,
))
->condition('lid', $lid)
->execute();
$exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField();
Gábor Hojtsy
committed
if (!$exists) {
// No translation in this language.
Dries Buytaert
committed
db_insert('locales_target')
->fields(array(
'lid' => $lid,
'language' => $langcode,
'translation' => $translation,
'plid' => $plid,
'plural' => $plural,
))
->execute();
Dries Buytaert
committed
$report['additions']++;
Gábor Hojtsy
committed
}
Angie Byron
committed
elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
// Translation exists, only overwrite if instructed.
Dries Buytaert
committed
db_update('locales_target')
->fields(array(
'translation' => $translation,
'plid' => $plid,
'plural' => $plural,
))
->condition('language', $langcode)
->condition('lid', $lid)
->execute();
Dries Buytaert
committed
$report['updates']++;
Gábor Hojtsy
committed
}
}
else {
// No such source string in the database yet.
Dries Buytaert
committed
$lid = db_insert('locales_source')
Dries Buytaert
committed
->fields(array(
'location' => $location,
'source' => $source,
'context' => (string) $context,
'textgroup' => $textgroup,
))
Dries Buytaert
committed
->execute();
db_insert('locales_target')
->fields(array(
'lid' => $lid,
'language' => $langcode,
'translation' => $translation,
'plid' => $plid,
'plural' => $plural
))
->execute();
Dries Buytaert
committed
$report['additions']++;
Gábor Hojtsy
committed
}
}
elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
// Empty translation, remove existing if instructed.
Dries Buytaert
committed
db_delete('locales_target')
->condition('language', $langcode)
->condition('lid', $lid)
->condition('plid', $plid)
->condition('plural', $plural)
->execute();
Dries Buytaert
committed
$report['deletes']++;
Gábor Hojtsy
committed
}
Gábor Hojtsy
committed
return $lid;
}
* @param $header
* A string containing the complete header
* @return
* An associative array of key-value pairs
Gábor Hojtsy
committed
$header_parsed = array();
$lines = array_map('trim', explode("\n", $header));
foreach ($lines as $line) {
if ($line) {
list($tag, $contents) = explode(":", $line, 2);
Gábor Hojtsy
committed
$header_parsed[trim($tag)] = trim($contents);
Gábor Hojtsy
committed
return $header_parsed;
}
/**
* Parses a Plural-Forms entry from a Gettext Portable Object file header
*
* @param $pluralforms
* A string containing the Plural-Forms entry
Dries Buytaert
committed
* @param $filepath
* A string containing the filepath
* @return
* An array containing the number of plurals and a
* formula in PHP for computing the plural form
Dries Buytaert
committed
function _locale_import_parse_plural_forms($pluralforms, $filepath) {
// First, delete all whitespace
$pluralforms = strtr($pluralforms, array(" " => "", "\t" => ""));
// Select the parts that define nplurals and plural
$nplurals = strstr($pluralforms, "nplurals=");
if (strpos($nplurals, ";")) {
$nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9);
}
else {
return FALSE;
}
$plural = strstr($pluralforms, "plural=");
if (strpos($plural, ";")) {
$plural = substr($plural, 7, strpos($plural, ";") - 7);
}
else {
return FALSE;
}
// Get PHP version of the plural formula
$plural = _locale_import_parse_arithmetic($plural);
if ($plural !== FALSE) {
Dries Buytaert
committed
drupal_set_message(t('The translation file %filepath contains an error: the plural formula could not be parsed.', array('%filepath' => $filepath)), 'error');
return FALSE;
}
}
/**
* Parses and sanitizes an arithmetic formula into a PHP expression
*
* While parsing, we ensure, that the operators have the right
* precedence and associativity.
*
* @param $string
* A string containing the arithmetic formula
* @return
* The PHP version of the formula
*/
function _locale_import_parse_arithmetic($string) {
// Operator precedence table
$precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8);
$right_associativity = array("?" => 1, ":" => 1);
$tokens = _locale_import_tokenize_formula($string);
// Parse by converting into infix notation then back into postfix
// Operator stack - holds math operators and symbols
$operator_stack = array();
// Element Stack - holds data to be operated on
$element_stack = array();
$current_token = $token;
// Numbers and the $n variable are simply pushed into $element_stack
$element_stack[] = $current_token;
elseif ($current_token == "n") {
$element_stack[] = '$n';
elseif ($current_token == "(") {
$operator_stack[] = $current_token;
elseif ($current_token == ")") {
$topop = array_pop($operator_stack);
while (isset($topop) && ($topop != "(")) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
elseif (!empty($precedence[$current_token])) {
// If it's an operator, then pop from $operator_stack into $element_stack until the
// precedence in $operator_stack is less than current, then push into $operator_stack
$topop = array_pop($operator_stack);
while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
$operator_stack[] = $topop; // Return element to top
$operator_stack[] = $current_token; // Parentheses are not needed
return FALSE;
$topop = array_pop($operator_stack);
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
$previous_size = count($element_stack) + 1;
while (count($element_stack) < $previous_size) {
$previous_size = count($element_stack);
for ($i = 2; $i < count($element_stack); $i++) {
$op = $element_stack[$i];