diff --git a/core/includes/gettext.inc b/core/includes/gettext.inc deleted file mode 100644 index ef9d25d..0000000 --- a/core/includes/gettext.inc +++ /dev/null @@ -1,1131 +0,0 @@ - $file->filename)), 'error'); - } - - // Clear cache and force refresh of JavaScript translations. - _locale_invalidate_js($langcode); - cache()->deletePrefix('locale:'); - - // Rebuild the menu, strings may have changed. - menu_router_rebuild(); - - drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes))); - watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes)); - if ($skips) { - if (module_exists('dblog')) { - $skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => url('admin/reports/dblog'))); - } - else { - $skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.'); - } - drupal_set_message($skip_message, 'error'); - watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING); - } - return TRUE; -} - -/** - * Parses a 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 $overwrite_options - * An associative array indicating what data should be overwritten, if any. - * - not_customized: not customized strings should be overwritten. - * - customized: customized strings should be overwritten. - * @param $lang - * Language code. - * @param $customized - * Whether the strings being imported should be saved as customized. - * Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. - */ -function _locale_import_read_po($op, $file, $overwrite_options = NULL, $lang = NULL, $customized = LOCALE_NOT_CUSTOMIZED) { - - // The file will get closed by PHP on returning from this function. - $fd = fopen($file->uri, 'rb'); - if (!$fd) { - _locale_import_message('The translation import failed because the file %filename could not be read.', $file); - return FALSE; - } - - /* - * The parser context. Can be: - * - 'COMMENT' (#) - * - 'MSGID' (msgid) - * - 'MSGID_PLURAL' (msgid_plural) - * - 'MSGCTXT' (msgctxt) - * - 'MSGSTR' (msgstr or msgstr[]) - * - 'MSGSTR_ARR' (msgstr_arg) - */ - $context = 'COMMENT'; - - // Current entry being read. - $current = array(); - - // Current plurality for 'msgstr[]'. - $plural = 0; - - // Current line. - $lineno = 0; - - while (!feof($fd)) { - // A line should not be longer than 10 * 1024. - $line = fgets($fd, 10 * 1024); - - if ($lineno == 0) { - // The first line might come with a UTF-8 BOM, which should be removed. - $line = str_replace("\xEF\xBB\xBF", '', $line); - } - - $lineno++; - - // Trim away the linefeed. - $line = trim(strtr($line, array("\\\n" => ""))); - - if (!strncmp('#', $line, 1)) { - // Lines starting with '#' are comments. - - if ($context == 'COMMENT') { - // Already in comment token, insert the comment. - $current['#'][] = substr($line, 1); - } - elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - // We are currently in string token, close it out. - _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized); - - // Start a new entry for the comment. - $current = array(); - $current['#'][] = substr($line, 1); - - $context = 'COMMENT'; - } - else { - // A comment following any other token is a syntax error. - _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno); - return FALSE; - } - } - elseif (!strncmp('msgid_plural', $line, 12)) { - // A plural form for the current message. - - if ($context != 'MSGID') { - // A plural form cannot be added to anything else but the id directly. - _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno); - return FALSE; - } - - // Remove 'msgid_plural' and trim away whitespace. - $line = trim(substr($line, 12)); - // At this point, $line should now contain only the plural form. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The plural form must be wrapped in quotes. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - // Append the plural form to the current entry. - $current['msgid'] .= LOCALE_PLURAL_DELIMITER . $quoted; - - $context = 'MSGID_PLURAL'; - } - elseif (!strncmp('msgid', $line, 5)) { - // Starting a new message. - - if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - // We are currently in a message string, close it out. - _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized); - - // Start a new context for the id. - $current = array(); - } - elseif ($context == 'MSGID') { - // We are currently already in the context, meaning we passed an id with no data. - _locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno); - return FALSE; - } - - // Remove 'msgid' and trim away whitespace. - $line = trim(substr($line, 5)); - // At this point, $line should now contain only the message id. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The message id must be wrapped in quotes. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - $current['msgid'] = $quoted; - $context = 'MSGID'; - } - elseif (!strncmp('msgctxt', $line, 7)) { - // Starting a new context. - - if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - // We are currently in a message, start a new one. - _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized); - $current = array(); - } - elseif (!empty($current['msgctxt'])) { - // A context cannot apply to another context. - _locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno); - return FALSE; - } - - // Remove 'msgctxt' and trim away whitespaces. - $line = trim(substr($line, 7)); - // At this point, $line should now contain the context. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The context string must be quoted. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - $current['msgctxt'] = $quoted; - - $context = 'MSGCTXT'; - } - elseif (!strncmp('msgstr[', $line, 7)) { - // A message string for a specific plurality. - - if (($context != 'MSGID') && ($context != 'MSGCTXT') && ($context != 'MSGID_PLURAL') && ($context != 'MSGSTR_ARR')) { - // Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries. - _locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno); - return FALSE; - } - - // Ensure the plurality is terminated. - if (strpos($line, ']') === FALSE) { - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - // Extract the plurality. - $frombracket = strstr($line, '['); - $plural = substr($frombracket, 1, strpos($frombracket, ']') - 1); - - // Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data. - $line = trim(strstr($line, " ")); - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The string must be quoted. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - $current['msgstr'][$plural] = $quoted; - - $context = 'MSGSTR_ARR'; - } - elseif (!strncmp("msgstr", $line, 6)) { - // A string for the an id or context. - - if (($context != 'MSGID') && ($context != 'MSGCTXT')) { - // Strings are only valid within an id or context scope. - _locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno); - return FALSE; - } - - // Remove 'msgstr' and trim away away whitespaces. - $line = trim(substr($line, 6)); - // At this point, $line should now contain the message. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The string must be quoted. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - $current['msgstr'] = $quoted; - - $context = 'MSGSTR'; - } - elseif ($line != '') { - // Anything that is not a token may be a continuation of a previous token. - - $quoted = _locale_import_parse_quoted($line); - if ($quoted === FALSE) { - // The string must be quoted. - _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno); - return FALSE; - } - - // Append the string to the current context. - if (($context == 'MSGID') || ($context == 'MSGID_PLURAL')) { - $current['msgid'] .= $quoted; - } - elseif ($context == 'MSGCTXT') { - $current['msgctxt'] .= $quoted; - } - elseif ($context == 'MSGSTR') { - $current['msgstr'] .= $quoted; - } - elseif ($context == 'MSGSTR_ARR') { - $current['msgstr'][$plural] .= $quoted; - } - else { - // No valid context to append to. - _locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno); - return FALSE; - } - } - } - - // End of PO file, closed out the last entry. - if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) { - _locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized); - } - elseif ($context != 'COMMENT') { - _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno); - return FALSE; - } -} - -/** - * Sets an error message if an error occurred during locale file parsing. - * - * @param $message - * The message to be translated. - * @param $file - * Drupal file object corresponding to the PO file to import. - * @param $lineno - * An optional line number argument. - */ -function _locale_import_message($message, $file, $lineno = NULL) { - $vars = array('%filename' => $file->filename); - if (isset($lineno)) { - $vars['%line'] = $lineno; - } - $t = get_t(); - drupal_set_message($t($message, $vars), 'error'); -} - -/** - * Performs the specified operation for one string. - * - * @param $op - * Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'. - * @param $value - * Details of the string stored. - * @param $overwrite_options - * An associative array indicating what data should be overwritten, if any. - * - not_customized: not customized strings should be overwritten. - * - customized: customized strings should be overwritten. - * @param $lang - * Language to store the string in. - * @param $file - * Object representation of file being imported, only required when op is - * 'db-store'. - * @param $customized - * Whether the strings being imported should be saved as customized. - * Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. - */ -function _locale_import_one_string($op, $value = NULL, $overwrite_options = NULL, $lang = NULL, $file = NULL, $customized = LOCALE_NOT_CUSTOMIZED) { - $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0)); - $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE); - $strings = &drupal_static(__FUNCTION__ . ':strings', array()); - - switch ($op) { - // Return stored strings - case 'mem-report': - return $strings; - - // Store string in memory (only supports single strings) - case 'mem-store': - $strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr']; - return; - - // Called at end of import to inform the user - case 'db-report': - return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']); - - // Store the string we got in the database. - case 'db-store': - - if ($value['msgid'] == '') { - // If 'msgid' is empty, it means we got values for the header of the - // file as per the structure of the Gettext format. - $locale_plurals = variable_get('locale_translation_plurals', array()); - if (array_sum($overwrite_options) || empty($locale_plurals[$lang]['plurals'])) { - // Since we only need to parse the header if we ought to update the - // plural formula, only run this if we don't need to keep existing - // data untouched or if we don't have an existing plural formula. - $header = _locale_import_parse_header($value['msgstr']); - - // Get and store the plural formula if available. - if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) { - list($nplurals, $formula) = $p; - $locale_plurals[$lang] = array( - 'plurals' => $nplurals, - 'formula' => $formula, - ); - variable_set('locale_translation_plurals', $locale_plurals); - } - } - $header_done = TRUE; - } - - else { - // Found a string to store, clean up and prepare the data. - $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']); - - if (is_array($value['msgstr'])) { - // Sort plural variants by their form index. - ksort($value['msgstr']); - // Serialize plural variants in one string by LOCALE_PLURAL_DELIMITER. - $value['msgstr'] = implode(LOCALE_PLURAL_DELIMITER, $value['msgstr']); - } - - _locale_import_one_string_db( - $report, - $lang, - isset($value['msgctxt']) ? $value['msgctxt'] : '', - $value['msgid'], - $value['msgstr'], - $comments, - $overwrite_options, - $customized - ); - } - } // end of db-store operation -} - -/** - * Imports 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. - * @param $context - * The context of this string. - * @param $source - * Source string. - * @param $translation - * Translation to language specified in $langcode. - * @param $location - * Location value to save with source string. - * @param $overwrite_options - * An associative array indicating what data should be overwritten, if any. - * - not_customized: not customized strings should be overwritten. - * - customized: customized strings should be overwritten. - * @param $customized - * (optional) Whether the strings being imported should be saved as customized. - * Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. - * - * @return - * The string ID of the existing string modified or the new string added. - */ -function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) { - - // Initialize overwrite options if not set. - $overwrite_options += array( - 'not_customized' => FALSE, - 'customized' => FALSE, - ); - - // Look up the source string and any existing translation. - $string = db_query("SELECT s.lid, t.customized FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context", array( - ':source' => $source, - ':context' => $context, - ':language' => $langcode, - )) - ->fetchObject(); - - if (!empty($translation)) { - // Skip this string unless it passes a check for dangerous code. - if (!locale_string_is_safe($translation)) { - watchdog('locale', 'Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $translation), WATCHDOG_ERROR); - $report['skips']++; - return 0; - } - elseif (isset($string->lid)) { - // We have this source string saved already. - db_update('locales_source') - ->fields(array( - 'location' => $location, - )) - ->condition('lid', $string->lid) - ->execute(); - - if (!isset($string->customized)) { - // No translation in this language. - db_insert('locales_target') - ->fields(array( - 'lid' => $string->lid, - 'language' => $langcode, - 'translation' => $translation, - 'customized' => $customized, - )) - ->execute(); - - $report['additions']++; - } - elseif ($overwrite_options[$string->customized ? 'customized' : 'not_customized']) { - // Translation exists, only overwrite if instructed. - db_update('locales_target') - ->fields(array( - 'translation' => $translation, - 'customized' => $customized, - )) - ->condition('language', $langcode) - ->condition('lid', $string->lid) - ->execute(); - - $report['updates']++; - } - return $string->lid; - } - else { - // No such source string in the database yet. - $lid = db_insert('locales_source') - ->fields(array( - 'location' => $location, - 'source' => $source, - 'context' => (string) $context, - )) - ->execute(); - - db_insert('locales_target') - ->fields(array( - 'lid' => $lid, - 'language' => $langcode, - 'translation' => $translation, - 'customized' => $customized, - )) - ->execute(); - - $report['additions']++; - return $lid; - } - } - elseif (isset($string->lid) && isset($string->customized) && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) { - // Empty translation, remove existing if instructed. - db_delete('locales_target') - ->condition('language', $langcode) - ->condition('lid', $string->lid) - ->execute(); - - $report['deletes']++; - return $string->lid; - } -} - -/** - * Parses a Gettext Portable Object file header. - * - * @param $header - * A string containing the complete header. - * - * @return - * An associative array of key-value pairs. - */ -function _locale_import_parse_header($header) { - $header_parsed = array(); - $lines = array_map('trim', explode("\n", $header)); - foreach ($lines as $line) { - if ($line) { - list($tag, $contents) = explode(":", $line, 2); - $header_parsed[trim($tag)] = trim($contents); - } - } - return $header_parsed; -} - -/** - * Parses a Plural-Forms entry from a Gettext Portable Object file header. - * - * @param $pluralforms - * A string containing the Plural-Forms entry. - * @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. - */ -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) { - return array($nplurals, $plural); - } - else { - 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 - $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(); - - foreach ($tokens as $token) { - $current_token = $token; - - // Numbers and the $n variable are simply pushed into $element_stack - if (is_numeric($token)) { - $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); - } - if ($topop) { - $operator_stack[] = $topop; // Return element to top - } - $operator_stack[] = $current_token; // Parentheses are not needed - } - else { - return FALSE; - } - } - - // Flush operator stack - $topop = array_pop($operator_stack); - while ($topop != NULL) { - $element_stack[] = $topop; - $topop = array_pop($operator_stack); - } - - // Now extract formula from 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]; - if (!empty($precedence[$op])) { - $f = ""; - if ($op == ":") { - $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")"; - } - elseif ($op == "?") { - $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1]; - } - else { - $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")"; - } - array_splice($element_stack, $i - 2, 3, $f); - break; - } - } - } - - // If only one element is left, the number of operators is appropriate - if (count($element_stack) == 1) { - return $element_stack[0]; - } - else { - return FALSE; - } -} - -/** - * Provides backward-compatible formula parsing for token_get_all(). - * - * @param $string - * A string containing the arithmetic formula. - * - * @return - * The PHP version of the formula. - */ -function _locale_import_tokenize_formula($formula) { - $formula = str_replace(" ", "", $formula); - $tokens = array(); - for ($i = 0; $i < strlen($formula); $i++) { - if (is_numeric($formula[$i])) { - $num = $formula[$i]; - $j = $i + 1; - while ($j < strlen($formula) && is_numeric($formula[$j])) { - $num .= $formula[$j]; - $j++; - } - $i = $j - 1; - $tokens[] = $num; - } - elseif ($pos = strpos(" =<>!&|", $formula[$i])) { // We won't have a space - $next = $formula[$i + 1]; - switch ($pos) { - case 1: - case 2: - case 3: - case 4: - if ($next == '=') { - $tokens[] = $formula[$i] . '='; - $i++; - } - else { - $tokens[] = $formula[$i]; - } - break; - case 5: - if ($next == '&') { - $tokens[] = '&&'; - $i++; - } - else { - $tokens[] = $formula[$i]; - } - break; - case 6: - if ($next == '|') { - $tokens[] = '||'; - $i++; - } - else { - $tokens[] = $formula[$i]; - } - break; - } - } - else { - $tokens[] = $formula[$i]; - } - } - return $tokens; -} - -/** - * Generates a short, one-string version of the passed comment array. - * - * @param $comment - * An array of strings containing a comment. - * - * @return - * Short one-string version of the comment. - */ -function _locale_import_shorten_comments($comment) { - $comm = ''; - while (count($comment)) { - $test = $comm . substr(array_shift($comment), 1) . ', '; - if (strlen($comm) < 130) { - $comm = $test; - } - else { - break; - } - } - return trim(substr($comm, 0, -2)); -} - -/** - * Parses a string in quotes. - * - * @param $string - * A string specified with enclosing quotes. - * - * @return - * The string parsed from inside the quotes. - */ -function _locale_import_parse_quoted($string) { - if (substr($string, 0, 1) != substr($string, -1, 1)) { - return FALSE; // Start and end quotes must be the same - } - $quote = substr($string, 0, 1); - $string = substr($string, 1, -1); - if ($quote == '"') { // Double quotes: strip slashes - return stripcslashes($string); - } - elseif ($quote == "'") { // Simple quote: return as-is - return $string; - } - else { - return FALSE; // Unrecognized quote - } -} - -/** - * Generates a structured array of all translated strings for the language. - * - * @param $language - * Language object to generate the output for, or NULL if generating - * translation template. - * @param $options - * (optional) An associative array specifying what to include in the output: - * - customized: include customized strings (if TRUE) - * - uncustomized: include non-customized string (if TRUE) - * - untranslated: include untranslated source strings (if TRUE) - * Ignored if $language is NULL. - * - * @return - * An array of translated strings that can be used to generate an export. - */ -function _locale_export_get_strings($language = NULL, $options = array()) { - - // Assume FALSE for all options if not provided by the API. - $options += array( - 'customized' => FALSE, - 'not_customized' => FALSE, - 'not_translated' => FALSE, - ); - if (array_sum($options) == 0) { - // If user asked to not include anything in the translation files, - // that would not make sense, so just fall back on providing a template. - $language = NULL; - } - - // Build and execute query to collect source strings and translations. - $query = db_select('locales_source', 's'); - if (!empty($language)) { - if ($options['not_translated']) { - // Left join to keep untranslated strings in. - $query->leftJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $language->langcode)); - } - else { - // Inner join to filter for only translations. - $query->innerJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $language->langcode)); - } - if ($options['customized']) { - if (!$options['not_customized']) { - // Filter for customized strings only. - $query->condition('t.customized', LOCALE_CUSTOMIZED); - } - // Else no filtering needed in this case. - } - else { - if ($options['not_customized']) { - // Filter for non-customized strings only. - $query->condition('t.customized', LOCALE_NOT_CUSTOMIZED); - } - else { - // Filter for strings without translation. - $query->isNull('t.translation'); - } - } - $query->fields('t', array('translation')); - } - else { - $query->leftJoin('locales_target', 't', 's.lid = t.lid'); - } - $query->fields('s', array('lid', 'source', 'context', 'location')); - $result = $query->execute(); - - // Structure results in an array with metainformation on the strings. - $strings = array(); - foreach ($result as $child) { - $strings[$child->lid] = array( - 'comment' => $child->location, - 'source' => $child->source, - 'context' => $child->context, - 'translation' => isset($child->translation) ? $child->translation : '', - ); - } - return $strings; -} - -/** - * Generates the PO(T) file contents for the given strings. - * - * @param $language - * Language object to generate the output for, or NULL if generating - * translation template. - * @param $strings - * Array of strings to export. See _locale_export_get_strings() - * on how it should be formatted. - * @param $header - * The header portion to use for the output file. Defaults - * are provided for PO and POT files. - */ -function _locale_export_po_generate($language = NULL, $strings = array(), $header = NULL) { - global $user; - - $locale_plurals = variable_get('locale_translation_plurals', array()); - - if (!isset($header)) { - if (isset($language)) { - $header = '# ' . $language->name . ' translation of ' . config('system.site')->get('name') . "\n"; - $header .= '# Generated by ' . $user->name . ' <' . $user->mail . ">\n"; - $header .= "#\n"; - $header .= "msgid \"\"\n"; - $header .= "msgstr \"\"\n"; - $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; - $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; - $header .= "\"PO-Revision-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; - $header .= "\"Last-Translator: NAME \\n\"\n"; - $header .= "\"Language-Team: LANGUAGE \\n\"\n"; - $header .= "\"MIME-Version: 1.0\\n\"\n"; - $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; - $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; - if (!empty($locale_plurals[$language->langcode]['formula'])) { - $header .= "\"Plural-Forms: nplurals=" . $locale_plurals[$language->langcode]['plurals'] . "; plural=" . strtr($locale_plurals[$language->langcode]['formula'], array('$' => '')) . ";\\n\"\n"; - // Remember number of plural variants to optimize the export. - $nplurals = $locale_plurals[$language->langcode]['plurals']; - } - else { - // Remember we did not have a plural number for the export. - $nplurals = 0; - } - } - else { - $header = "# LANGUAGE translation of PROJECT\n"; - $header .= "# Copyright (c) YEAR NAME \n"; - $header .= "#\n"; - $header .= "msgid \"\"\n"; - $header .= "msgstr \"\"\n"; - $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; - $header .= "\"POT-Creation-Date: " . date("Y-m-d H:iO") . "\\n\"\n"; - $header .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; - $header .= "\"Last-Translator: NAME \\n\"\n"; - $header .= "\"Language-Team: LANGUAGE \\n\"\n"; - $header .= "\"MIME-Version: 1.0\\n\"\n"; - $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; - $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; - $header .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n"; - } - } - - $output = $header . "\n"; - - foreach ($strings as $lid => $string) { - if ($string['comment']) { - $output .= '#: ' . $string['comment'] . "\n"; - } - if (!empty($string['context'])) { - $output .= 'msgctxt ' . _locale_export_string($string['context']); - } - if (strpos($string['source'], LOCALE_PLURAL_DELIMITER) !== FALSE) { - // Export plural string. - $export_array = explode(LOCALE_PLURAL_DELIMITER, $string['source']); - $output .= 'msgid ' . _locale_export_string($export_array[0]); - $output .= 'msgid_plural ' . _locale_export_string($export_array[1]); - if (isset($language)) { - $export_array = explode(LOCALE_PLURAL_DELIMITER, $string['translation']); - for ($i = 0; $i < $nplurals; $i++) { - if (isset($export_array[$i])) { - $output .= 'msgstr[' . $i . '] ' . _locale_export_string($export_array[$i]); - } - else { - $output .= 'msgstr[' . $i . '] ""' . "\n"; - } - } - } - else { - $output .= 'msgstr[0] ""' . "\n"; - $output .= 'msgstr[1] ""' . "\n"; - } - } - else { - $output .= 'msgid ' . _locale_export_string($string['source']); - $output .= 'msgstr ' . _locale_export_string($string['translation']); - } - $output .= "\n"; - } - return $output; -} - -/** - * Writes a generated PO or POT file to the output. - * - * @param $language - * Language object to generate the output for, or NULL if generating - * translation template. - * @param $output - * The PO(T) file to output as a string. See _locale_export_generate_po() - * on how it can be generated. - */ -function _locale_export_po($language = NULL, $output = NULL) { - // Log the export event. - if (isset($language)) { - $filename = $language->langcode . '.po'; - watchdog('locale', 'Exported %locale translation file: %filename.', array('%locale' => $language->name, '%filename' => $filename)); - } - else { - $filename = 'drupal.pot'; - watchdog('locale', 'Exported translation file: %filename.', array('%filename' => $filename)); - } - // Download the file for the client. - header("Content-Disposition: attachment; filename=$filename"); - header("Content-Type: text/plain; charset=utf-8"); - print $output; - drupal_exit(); -} - -/** - * Prints a string on multiple lines. - */ -function _locale_export_string($str) { - $stri = addcslashes($str, "\0..\37\\\""); - $parts = array(); - - // Cut text into several lines - while ($stri != "") { - $i = strpos($stri, "\\n"); - if ($i === FALSE) { - $curstr = $stri; - $stri = ""; - } - else { - $curstr = substr($stri, 0, $i + 2); - $stri = substr($stri, $i + 2); - } - $curparts = explode("\n", _locale_export_wrap($curstr, 70)); - $parts = array_merge($parts, $curparts); - } - - // Multiline string - if (count($parts) > 1) { - return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n"; - } - // Single line string - elseif (count($parts) == 1) { - return "\"$parts[0]\"\n"; - } - // No translation - else { - return "\"\"\n"; - } -} - -/** - * Wraps text for Portable Object (Template) files. - */ -function _locale_export_wrap($str, $len) { - $words = explode(' ', $str); - $return = array(); - - $cur = ""; - $nstr = 1; - while (count($words)) { - $word = array_shift($words); - if ($nstr) { - $cur = $word; - $nstr = 0; - } - elseif (strlen("$cur $word") > $len) { - $return[] = $cur . " "; - $cur = $word; - } - else { - $cur = "$cur $word"; - } - } - $return[] = $cur; - - return implode("\n", $return); -} - -/** - * @} End of "defgroup locale-api-import-export". - */ diff --git a/core/includes/install.inc b/core/includes/install.inc index 41d244a..71146e2 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -1,6 +1,7 @@ 1);\n" + */ +class PoHeader { + + /** + * Language code. + * + * @var string + */ + private $_langcode; + + /** + * Formula for the plural form. + * + * @var string + */ + private $_pluralForms; + + /** + * Author(s) of the file. + * + * @var string + */ + private $_authors; + + /** + * Date the po file got created. + * + * @var string + */ + private $_po_date; + + /** + * Human readable language name. + * + * @var string + */ + private $_languageName; + + /** + * Name of the project the translation belongs to. + * + * @var string + */ + private $_projectName; + + /** + * Constructor, creates a PoHeader with default values set. + * + * @param string $langcode + * Language code. + */ + public function __construct($langcode = NULL) { + $this->_langcode = $langcode; + $this->_po_date = date("Y-m-d H:iO"); + $this->_pluralForms = 'nplurals=2; plural=(n > 1);'; + } + + /** + * Get the plural form. + * + * @return string + * Plural form component from the header, for example: + * 'nplurals=2; plural=(n > 1);'. + */ + function getPluralForms() { + return $this->_pluralForms; + } + + /** + * Set the human readable language name. + * + * @param string $languageName + * Human readable language name. + */ + function setLanguageName($languageName) { + $this->_languageName = $languageName; + } + + /** + * Get the human readable language name. + * + * @return string + * The human readable language name. + */ + function getLanguageName() { + return $this->_languageName; + } + + /** + * Set the project name. + * + * @param string $projectName + * Human readable project name. + */ + function setProjectName($projectName) { + $this->_projectName = $projectName; + } + + /** + * Get the project name. + * + * @return string + * The human readable project name. + */ + function getProjectName() { + return $this->_projectName; + } + + /** + * Populate internal values from a string. + * + * @param string $header + * Full header string with key-value pairs. + */ + public function setFromString($header) { + // Get an array of all header values for processing. + $values = $this->parseHeader($header); + + // There is only one value relevant for our header implementation when + // reading, and that is the plural formula. + if (!empty($values['Plural-Forms'])) { + $this->_pluralForms = $values['Plural-Forms']; + } + } + + /** + * Generate a Gettext PO formatted header string based on data set earlier. + */ + public function __toString() { + $output = ''; + + $isTemplate = empty($this->_languageName); + + $output .= '# ' . ($isTemplate ? 'LANGUAGE' : $this->_languageName) . ' translation of ' . ($isTemplate ? 'PROJECT' : $this->_projectName) . "\n"; + if (!empty($this->_authors)) { + $output .= '# Generated by ' . implode("\n# ", $this->_authors) . "\n"; + } + $output .= "#\n"; + + // Add the actual header information. + $output .= "msgid \"\"\n"; + $output .= "msgstr \"\"\n"; + $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $output .= "\"POT-Creation-Date: " . $this->_po_date . "\\n\"\n"; + $output .= "\"PO-Revision-Date: " . $this->_po_date . "\\n\"\n"; + $output .= "\"Last-Translator: NAME \\n\"\n"; + $output .= "\"Language-Team: LANGUAGE \\n\"\n"; + $output .= "\"MIME-Version: 1.0\\n\"\n"; + $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + $output .= "\"Plural-Forms: " . $this->_pluralForms . "\\n\"\n"; + $output .= "\n"; + + return $output; + } + + /** + * Parses a Plural-Forms entry from a Gettext Portable Object file header. + * + * @param string $pluralforms + * The Plural-Forms entry value. + * + * @return + * An array containing the number of plural forms and the converted version + * of the formula that can be evaluated with PHP later. + */ + function parsePluralForms($pluralforms) { + // 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 = $this->parseArithmetic($plural); + + if ($plural !== FALSE) { + return array($nplurals, $plural); + } + else { + throw new Exception('The plural formula could not be parsed.'); + } + } + + /** + * Parses a Gettext Portable Object file header. + * + * @param string $header + * A string containing the complete header. + * + * @return array + * An associative array of key-value pairs. + */ + private function parseHeader($header) { + $header_parsed = array(); + $lines = array_map('trim', explode("\n", $header)); + foreach ($lines as $line) { + if ($line) { + list($tag, $contents) = explode(":", $line, 2); + $header_parsed[trim($tag)] = trim($contents); + } + } + return $header_parsed; + } + + /** + * 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 $string + * A string containing the arithmetic formula. + * + * @return + * A version of the formula to evaluate with PHP later. + */ + private function parseArithmetic($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. + $right_associativity = array("?" => 1, ":" => 1); + + $tokens = $this->tokenizeFormula($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(); + + foreach ($tokens as $token) { + $current_token = $token; + + // Numbers and the $n variable are simply pushed into $element_stack. + if (is_numeric($token)) { + $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); + } + if ($topop) { + // Return element to top. + $operator_stack[] = $topop; + } + // Parentheses are not needed. + $operator_stack[] = $current_token; + } + else { + return FALSE; + } + } + + // Flush operator stack. + $topop = array_pop($operator_stack); + while ($topop != NULL) { + $element_stack[] = $topop; + $topop = array_pop($operator_stack); + } + + // Now extract formula from 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]; + if (!empty($precedence[$op])) { + $f = ""; + if ($op == ":") { + $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")"; + } + elseif ($op == "?") { + $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1]; + } + else { + $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")"; + } + array_splice($element_stack, $i - 2, 3, $f); + break; + } + } + } + + // If only one element is left, the number of operators is appropriate. + if (count($element_stack) == 1) { + return $element_stack[0]; + } + else { + return FALSE; + } + } + + /** + * Tokenize the formula. + * + * @param string $formula + * A string containing the arithmetic formula. + * + * @return array + * List of arithmetic tokens identified in the formula. + */ + private function tokenizeFormula($formula) { + $formula = str_replace(" ", "", $formula); + $tokens = array(); + for ($i = 0; $i < strlen($formula); $i++) { + if (is_numeric($formula[$i])) { + $num = $formula[$i]; + $j = $i + 1; + while ($j < strlen($formula) && is_numeric($formula[$j])) { + $num .= $formula[$j]; + $j++; + } + $i = $j - 1; + $tokens[] = $num; + } + elseif ($pos = strpos(" =<>!&|", $formula[$i])) { + $next = $formula[$i + 1]; + switch ($pos) { + case 1: + case 2: + case 3: + case 4: + if ($next == '=') { + $tokens[] = $formula[$i] . '='; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + case 5: + if ($next == '&') { + $tokens[] = '&&'; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + case 6: + if ($next == '|') { + $tokens[] = '||'; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + } + } + else { + $tokens[] = $formula[$i]; + } + } + return $tokens; + } + +} diff --git a/core/lib/Drupal/Component/Gettext/PoItem.php b/core/lib/Drupal/Component/Gettext/PoItem.php new file mode 100644 index 0000000..599a58a --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoItem.php @@ -0,0 +1,287 @@ +_langcode; + } + + /** + * Set the language code of the current language. + * + * @param string $langcode + */ + function setLangcode($langcode) { + $this->_langcode = $langcode; + } + + /** + * Get the context this translation belongs to. + * + * @return string $context + */ + function getContext() { + return $this->_context; + } + + /** + * Set the context this translation belongs to. + * + * @param string $context + */ + function setContext($context) { + $this->_context = $context; + } + + /** + * Get the source string or the array of strings if the translation has + * plurals. + * + * @return string or array $translation + */ + function getSource() { + return $this->_source; + } + + /** + * Set the source string or the array of strings if the translation has + * plurals. + * + * @param string or array $source + */ + function setSource($source) { + $this->_source = $source; + } + + /** + * Get the translation string or the array of strings if the translation has + * plurals. + * + * @return string or array $translation + */ + function getTranslation() { + return $this->_translation; + } + + /** + * Set the translation string or the array of strings if the translation has + * plurals. + * + * @param string or array $translation + */ + function setTranslation($translation) { + $this->_translation = $translation; + } + + /** + * Set if the translation has plural values. + * + * @param boolean $plural + */ + function setPlural($plural) { + $this->_plural = $plural; + } + + /** + * Get if the translation has plural values. + * + * @return boolean $plural + */ + function isPlural() { + return $this->_plural; + } + + /** + * Get the comment of this translation. + * + * @return String $comment + */ + function getComment() { + return $this->_comment; + } + + /** + * Set the comment of this translation. + * + * @param String $comment + */ + function setComment($comment) { + $this->_comment = $comment; + } + + /** + * Create the PoItem from a structured array. + * + * @param array values + */ + public function setFromArray(array $values = array()) { + if (isset($values['context'])) { + $this->setContext($values['context']); + } + if (isset($values['source'])) { + $this->setSource($values['source']); + } + if (isset($values['translation'])) { + $this->setTranslation($values['translation']); + } + if (isset($values['comment'])){ + $this->setComment($values['comment']); + } + if (isset($this->_source) && + strpos($this->_source, LOCALE_PLURAL_DELIMITER) !== FALSE) { + $this->setSource(explode(LOCALE_PLURAL_DELIMITER, $this->_source)); + $this->setTranslation(explode(LOCALE_PLURAL_DELIMITER, $this->_translation)); + $this->setPlural(count($this->_translation) > 1); + } + } + + /** + * Output the PoItem as a string. + */ + public function __toString() { + return $this->formatItem(); + } + + /** + * Format the POItem as a string. + */ + private function formatItem() { + $output = ''; + + // Format string context. + if (!empty($this->_context)) { + $output .= 'msgctxt ' . $this->formatString($this->_context); + } + + // Format translation. + if ($this->_plural) { + $output .= $this->formatPlural(); + } + else { + $output .= $this->formatSingular(); + } + + // Add one empty line to separate the translations. + $output .= "\n"; + + return $output; + } + + /** + * Formats a plural translation. + */ + private function formatPlural() { + $output = ''; + + // Format source strings. + $output .= 'msgid ' . $this->formatString($this->_source[0]); + $output .= 'msgid_plural ' . $this->formatString($this->_source[1]); + + foreach ($this->_translation as $i => $trans) { + if (isset($this->_translation[$i])) { + $output .= 'msgstr[' . $i . '] ' . $this->formatString($trans); + } + else { + $output .= 'msgstr[' . $i . '] ""' . "\n"; + } + } + + return $output; + } + + /** + * Formats a singular translation. + */ + private function formatSingular() { + $output = ''; + $output .= 'msgid '. $this->formatString($this->_source); + $output .= 'msgstr '. (isset($this->_translation) ? $this->formatString($this->_translation) : ''); + return $output; + } + + /** + * Formats a string for output on multiple lines. + */ + private function formatString($string) { + // Escape characters for processing. + $string = addcslashes($string, "\0..\37\\\""); + + // Always include a line break after the explicit \n line breaks from + // the source string. Otherwise wrap at 70 chars to accommodate the extra + // format overhead too. + $parts = explode("\n", wordwrap(str_replace('\n', "\\n\n", $string), 70, " \n")); + + // Multiline string should be exported starting with a "" and newline to + // have all lines aligned on the same column. + if (count($parts) > 1) { + return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n"; + } + // Single line strings are output on the same line. + else { + return "\"$parts[0]\"\n"; + } + } + +} diff --git a/core/lib/Drupal/Component/Gettext/PoMemoryWriter.php b/core/lib/Drupal/Component/Gettext/PoMemoryWriter.php new file mode 100644 index 0000000..04087fb --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoMemoryWriter.php @@ -0,0 +1,95 @@ +_items = array(); + } + + /** + * Implements Drupal\Component\Gettext\PoWriterInterface::writeItem(). + */ + public function writeItem(PoItem $item) { + if (is_array($item->source)) { + $item->source = implode(LOCALE_PLURAL_DELIMITER, $item->source); + $item->translation = implode(LOCALE_PLURAL_DELIMITER, $item->translation); + } + $this->_items[isset($item->context) ? $item->context : ''][$item->source] = $item->translation; + } + + /** + * Implements Drupal\Component\Gettext\PoWriterInterface::writeItems(). + */ + public function writeItems(PoReaderInterface $reader, $count = -1) { + $forever = $count == -1; + while (($count-- > 0 || $forever) && ($item = $reader->readItem())) { + $this->writeItem($item); + } + } + + /** + * Get all stored PoItem's. + * + * @return array PoItem + */ + public function getData() { + return $this->_items; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface:setLangcode(). + * + * Not implemented. Not relevant for the installer. + */ + function setLangcode($langcode) { + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface:getLangcode(). + * + * Not implemented. Not relevant for the installer. + */ + function getLangcode() { + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface:getHeader(). + * + * Not implemented. Not relevant for the installer. + */ + function getHeader() { + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface:setHeader(). + * + * Not implemented. Not relevant for the installer. + */ + function setHeader(PoHeader $header) { + } + +} diff --git a/core/lib/Drupal/Component/Gettext/PoMetadataInterface.php b/core/lib/Drupal/Component/Gettext/PoMetadataInterface.php new file mode 100644 index 0000000..f5bb3db --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoMetadataInterface.php @@ -0,0 +1,51 @@ +_langcode; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode(). + */ + public function setLangcode($langcode) { + $this->_langcode = $langcode; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader(). + */ + public function getHeader() { + return $this->_header; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader(). + * + * Not applicable to stream reading and therefore not implemented. + */ + public function setHeader(PoHeader $header) { + } + + /** + * Implements Drupal\Component\Gettext\PoStreamInterface::getURI(). + */ + public function getURI() { + return $this->_uri; + } + + /** + * Implements Drupal\Component\Gettext\PoStreamInterface::setURI(). + */ + public function setURI($uri) { + $this->_uri = $uri; + } + + /** + * Implements Drupal\Component\Gettext\PoStreamInterface::open(). + * + * Opens the stream and reads the header. The stream is ready for reading + * items after. + * + * @throws Exception + * If the URI is not yet set. + */ + public function open() { + if (!empty($this->_uri)) { + $this->_fd = fopen($this->_uri, 'rb'); + $this->_size = ftell($this->_fd); + $this->readHeader(); + } + else { + throw new Exception('Cannot open stream without URI set.'); + } + } + + /** + * Implements Drupal\Component\Gettext\PoStreamInterface::close(). + * + * @throws Exception + * If the stream is not open. + */ + public function close() { + if ($this->_fd) { + fclose($this->_fd); + } + else { + throw new Exception('Cannot close stream that is not open.'); + } + } + + /** + * Implements Drupal\Component\Gettext\PoReaderInterface::readItem(). + */ + public function readItem() { + // Clear out the last item. + $this->_last_item = NULL; + + // Read until finished with the stream or a complete item was identified. + while (!$this->_finished && is_null($this->_last_item)) { + $this->readLine(); + } + + return $this->_last_item; + } + + /** + * Read the header from the PO stream. + * + * The header is a special case PoItem, using the empty string as source and + * key-value pairs as translation. We just reuse the item reader logic to + * read the header. + */ + private function readHeader() { + $item = $this->readItem(); + $header = new PoHeader; + $header->setFromString(trim($item->getTranslation())); + $this->_header = $header; + } + + /** + * Reads a line from the PO stream and stores data internally. + * + * Expands $this->_current_item based on new data for the current item. If + * this line ends the current item, it is saved with setItemFromArray() with + * data from $this->_current_item. + * + * An internal state machine is maintained in this reader using $this->_context + * as the reading state. PO items are inbetween COMMENT states (when items have + * at least one line or comment inbetween them or indicated by MSGSTR or + * MSGSTR_ARR followed immediately by an MSGID or MSGCTXT (when items closely + * follow each other). + * + * @return + * FALSE if an error was logged, NULL otherwise. The errors are considered + * non-blocking, so reading can continue, while the errors are collected + * for later presentation. + */ + private function readLine() { + // Read a line and set the stream finished indicator if it was not + // possible anymore. + $line = fgets($this->_fd); + $this->_finished = ($line === FALSE); + + if (!$this->_finished) { + + if ($this->_line_number == 0) { + // The first line might come with a UTF-8 BOM, which should be removed. + $line = str_replace("\xEF\xBB\xBF", '', $line); + // Current plurality for 'msgstr[]'. + $this->_current_plural_index = 0; + } + + // Track the line number for error reporting. + $this->_line_number++; + + // Initialize common values for error logging. + $log_vars = array( + '%uri' => $this->getURI(), + '%line' => $this->_line_number, + ); + $t = get_t(); + + // Trim away the linefeed. \\n might appear at the end of the string if + // another line continuing the same string follows. We can remove that. + $line = trim(strtr($line, array("\\\n" => ""))); + + if (!strncmp('#', $line, 1)) { + // Lines starting with '#' are comments. + + if ($this->_context == 'COMMENT') { + // Already in comment context, add to current comment. + $this->_current_item['#'][] = substr($line, 1); + } + elseif (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) { + // We are currently in string context, save current item. + $this->setItemFromArray($this->_current_item); + + // Start a new entry for the comment. + $this->_current_item = array(); + $this->_current_item['#'][] = substr($line, 1); + + $this->_context = 'COMMENT'; + return; + } + else { + // A comment following any other context is a syntax error. + $this->_errors[] = $t('The translation stream %uri contains an error: "msgstr" was expected but not found on line %line.', $log_vars); + return FALSE; + } + return; + } + elseif (!strncmp('msgid_plural', $line, 12)) { + // A plural form for the current source string. + + if ($this->_context != 'MSGID') { + // A plural form can only be added to an msgid directly. + $this->_errors[] = $t('The translation stream %uri contains an error: "msgid_plural" was expected but not found on line %line.', $log_vars); + return FALSE; + } + + // Remove 'msgid_plural' and trim away whitespace. + $line = trim(substr($line, 12)); + + // Only the plural source string is left, parse it. + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The plural form must be wrapped in quotes. + $this->_errors[] = $t('The translation stream %uri contains a syntax error on line %line.', $log_vars); + return FALSE; + } + + // Append the plural source to the current entry. + if (is_string($this->_current_item['msgid'])) { + // The first value was stored as string. Now we know the context is + // plural, it is converted to array. + $this->_current_item['msgid'] = array($this->_current_item['msgid']); + } + $this->_current_item['msgid'][] = $quoted; + + $this->_context = 'MSGID_PLURAL'; + return; + } + elseif (!strncmp('msgid', $line, 5)) { + // Starting a new message. + + if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) { + // We are currently in string context, save current item. + $this->setItemFromArray($this->_current_item); + + // Start a new context for the msgid. + $this->_current_item = array(); + } + elseif ($this->_context == 'MSGID') { + // We are currently already in the context, meaning we passed an id with no data. + $this->_errors[] = $t('The translation stream %uri contains an error: "msgid" is unexpected on line %line.', $log_vars); + return FALSE; + } + + // Remove 'msgid' and trim away whitespace. + $line = trim(substr($line, 5)); + + // Only the message id string is left, parse it. + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The message id must be wrapped in quotes. + $this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgid" on line %line.', $log_vars, $log_vars); + return FALSE; + } + + $this->_current_item['msgid'] = $quoted; + $this->_context = 'MSGID'; + return; + } + elseif (!strncmp('msgctxt', $line, 7)) { + // Starting a new context. + + if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) { + // We are currently in string context, save current item. + $this->setItemFromArray($this->_current_item); + $this->_current_item = array(); + } + elseif (!empty($this->_current_item['msgctxt'])) { + // A context cannot apply to another context. + $this->_errors[] = $t('The translation stream %uri contains an error: "msgctxt" is unexpected on line %line.', $log_vars); + return FALSE; + } + + // Remove 'msgctxt' and trim away whitespaces. + $line = trim(substr($line, 7)); + + // Only the msgctxt string is left, parse it. + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The context string must be quoted. + $this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgctxt" on line %line.', $log_vars); + return FALSE; + } + + $this->_current_item['msgctxt'] = $quoted; + + $this->_context = 'MSGCTXT'; + return; + } + elseif (!strncmp('msgstr[', $line, 7)) { + // A message string for a specific plurality. + + if (($this->_context != 'MSGID') && ($this->_context != 'MSGCTXT') && ($this->_context != 'MSGID_PLURAL') && ($this->_context != 'MSGSTR_ARR')) { + // Plural message strings must come after msgid, msgxtxt, + // msgid_plural, or other msgstr[] entries. + $this->_errors[] = $t('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars); + return FALSE; + } + + // Ensure the plurality is terminated. + if (strpos($line, ']') === FALSE) { + $this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars); + return FALSE; + } + + // Extract the plurality. + $frombracket = strstr($line, '['); + $this->_current_plural_index = substr($frombracket, 1, strpos($frombracket, ']') - 1); + + // Skip to the next whitespace and trim away any further whitespace, + // bringing $line to the message text only. + $line = trim(strstr($line, " ")); + + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + $this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars); + return FALSE; + } + if (!isset($this->_current_item['msgstr']) || !is_array($this->_current_item['msgstr'])) { + $this->_current_item['msgstr'] = array(); + } + + $this->_current_item['msgstr'][$this->_current_plural_index] = $quoted; + + $this->_context = 'MSGSTR_ARR'; + return; + } + elseif (!strncmp("msgstr", $line, 6)) { + // A string pair for an msgidid (with optional context). + + if (($this->_context != 'MSGID') && ($this->_context != 'MSGCTXT')) { + // Strings are only valid within an id or context scope. + $this->_errors[] = $t('The translation stream %uri contains an error: "msgstr" is unexpected on line %line.', $log_vars); + return FALSE; + } + + // Remove 'msgstr' and trim away away whitespaces. + $line = trim(substr($line, 6)); + + // Only the msgstr string is left, parse it. + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + $this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr" on line %line.', $log_vars); + return FALSE; + } + + $this->_current_item['msgstr'] = $quoted; + + $this->_context = 'MSGSTR'; + return; + } + elseif ($line != '') { + // Anything that is not a token may be a continuation of a previous token. + + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // This string must be quoted. + $this->_errors[] = $t('The translation stream %uri contains an error: string continuation expected on line %line.', $log_vars); + return FALSE; + } + + // Append the string to the current item. + if (($this->_context == 'MSGID') || ($this->_context == 'MSGID_PLURAL')) { + if (is_array($this->_current_item['msgid'])) { + // Add string to last array element for plural sources. + $last_index = count($this->_current_item['msgid']) - 1; + $this->_current_item['msgid'][$last_index] .= $quoted; + } + else { + // Singular source, just append the string. + $this->_current_item['msgid'] .= $quoted; + } + } + elseif ($this->_context == 'MSGCTXT') { + // Multiline context name. + $this->_current_item['msgctxt'] .= $quoted; + } + elseif ($this->_context == 'MSGSTR') { + // Multiline translation string. + $this->_current_item['msgstr'] .= $quoted; + } + elseif ($this->_context == 'MSGSTR_ARR') { + // Multiline plural translation string. + $this->_current_item['msgstr'][$this->_current_plural_index] .= $quoted; + } + else { + // No valid context to append to. + $this->_errors[] = $t('The translation stream %uri contains an error: unexpected string on line %line.', $log_vars); + return FALSE; + } + return; + } + } + + // Empty line read or EOF of PO stream, close out the last entry. + if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) { + $this->setItemFromArray($this->_current_item); + $this->_current_item = array(); + } + elseif ($this->_context != 'COMMENT') { + $this->_errors[] = $t('The translation stream %uri ended unexpectedly at line %line.', $log_vars); + return FALSE; + } + } + + /** + * Store the parsed values as a PoItem object. + */ + public function setItemFromArray($value) { + $plural = FALSE; + + $comments = ''; + if (isset($value['#'])) { + $comments = $this->shortenComments($value['#']); + } + + if (is_array($value['msgstr'])) { + // Sort plural variants by their form index. + ksort($value['msgstr']); + $plural = TRUE; + } + + $item = new PoItem(); + $item->setContext(isset($value['msgctxt']) ? $value['msgctxt'] : ''); + $item->setSource($value['msgid']); + $item->setTranslation($value['msgstr']); + $item->setPlural($plural); + $item->setComment($comments); + $item->setLangcode($this->_langcode); + + $this->_last_item = $item; + + $this->_context = 'COMMENT'; + } + + /** + * Parses a string in quotes. + * + * @param $string + * A string specified with enclosing quotes. + * + * @return + * The string parsed from inside the quotes. + */ + function parseQuoted($string) { + if (substr($string, 0, 1) != substr($string, -1, 1)) { + // Start and end quotes must be the same. + return FALSE; + } + $quote = substr($string, 0, 1); + $string = substr($string, 1, -1); + if ($quote == '"') { + // Double quotes: strip slashes. + return stripcslashes($string); + } + elseif ($quote == "'") { + // Simple quote: return as-is. + return $string; + } + else { + // Unrecognized quote. + return FALSE; + } + } + + /** + * Generates a short, one-string version of the passed comment array. + * + * @param $comment + * An array of strings containing a comment. + * + * @return + * Short one-string version of the comment. + */ + private function shortenComments($comment) { + $comm = ''; + while (count($comment)) { + $test = $comm . substr(array_shift($comment), 1) . ', '; + if (strlen($comm) < 130) { + $comm = $test; + } + else { + break; + } + } + return trim(substr($comm, 0, -2)); + } + +} diff --git a/core/lib/Drupal/Component/Gettext/PoStreamWriter.php b/core/lib/Drupal/Component/Gettext/PoStreamWriter.php new file mode 100644 index 0000000..d066307 --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoStreamWriter.php @@ -0,0 +1,168 @@ +_header; + } + + /** + * Set the PO header for the current stream. + * + * @param Drupal\Component\Gettext\PoHeader $header + * The Gettext PO header to set. + */ + public function setHeader(PoHeader $header) { + $this->_header = $header; + } + + /** + * Get the current language code used. + * + * @return string + * The language code. + */ + public function getLangcode() { + return $this->_langcode; + } + + /** + * Set the language code. + * + * @param string $langcode + * The language code. + */ + public function setLangcode($langcode) { + $this->_langcode = $langcode; + } + + /** + * Implements Drupal\Component\Gettext\PoStreamInterface::open(). + */ + public function open() { + // Open in write mode. Will overwrite the stream if it already exists. + $this->_fd = fopen($this->getURI(), 'w'); + // Write the header at the start. + $this->writeHeader(); + } + + /** + * Implements Drupal\Component\Gettext\PoStreamInterface::close(). + * + * @throws Exception + * If the stream is not open. + */ + public function close() { + if ($this->_fd) { + fclose($this->_fd); + } + else { + throw new Exception('Cannot close stream that is not open.'); + } + } + + /** + * Write data to the stream. + * + * @param string $data + * Piece of string to write to the stream. If the value is not directly a + * string, casting will happen in writing. + * + * @throws Exception + * If writing the data is not possible. + */ + private function write($data) { + $result = fputs($this->_fd, $data); + if ($result === FALSE) { + throw new Exception('Unable to write data: ' . substr($data, 0, 20)); + } + } + + /** + * Write the PO header to the stream. + */ + private function writeHeader() { + $this->write($this->_header); + } + + /** + * Implements Drupal\Component\Gettext\PoWriterInterface::writeItem(). + */ + public function writeItem(PoItem $item) { + $this->write($item); + } + + /** + * Implements Drupal\Component\Gettext\PoWriterInterface::writeItems(). + */ + public function writeItems(PoReaderInterface $reader, $count = -1) { + $forever = $count == -1; + while (($count-- > 0 || $forever) && ($item = $reader->readItem())) { + $this->writeItem($item); + } + } + + /** + * Implements Drupal\Component\Gettext\PoStreamInterface::getURI(). + * + * @throws Exception + * If the URI is not set. + */ + public function getURI() { + if (empty($this->_uri)) { + throw new Exception('No URI set.'); + } + return $this->_uri; + } + + /** + * Implements Drupal\Component\Gettext\PoStreamInterface::setURI(). + */ + public function setURI($uri) { + $this->_uri = $uri; + } + +} diff --git a/core/lib/Drupal/Component/Gettext/PoWriterInterface.php b/core/lib/Drupal/Component/Gettext/PoWriterInterface.php new file mode 100644 index 0000000..2a2348f --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoWriterInterface.php @@ -0,0 +1,36 @@ +setLangcode($langcode); + foreach ($files as $file) { + $reader = new PoStreamReader(); + $reader->setURI($file->uri); + $reader->setLangcode($langcode); + $reader->open(); + $writer->writeItems($reader, -1); + } + return $writer->getData(); + } + + /** + * Reads the given PO files into the database. + * + * @param stdClass $file + * File object with an uri property pointing at the file's path. + * @param string $langcode + * Language code string. + * @param array $overwrite_options + * Overwrite options array as defined in Drupal\locale\PoDatabaseWriter. + * @param boolean $customized + * Flag indicating whether the string imported from $file are customized + * translations or come from a community source. Use LOCALE_CUSTOMIZED or + * LOCALE_NOT_CUSTOMIZED. + * + * @return array + * Report array as defined in Drupal\locale\PoDatabaseWriter. + * + * @see Drupal\locale\PoDatabaseWriter + */ + static function fileToDatabase($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) { + // Instantiate and initialize the stream reader for this file. + $reader = new PoStreamReader(); + $reader->setLangcode($langcode); + $reader->setURI($file->uri); + + try { + $reader->open(); + } + catch (Exception $exception) { + throw $exception; + } + + $header = $reader->getHeader(); + if (!$header) { + throw new Exception('Missing or malformed header.'); + } + + // Initialize the database writer. + $writer = new PoDatabaseWriter(); + $writer->setLangcode($langcode); + $options = array( + 'overwrite_options' => $overwrite_options, + 'customized' => $customized, + ); + $writer->setOptions($options); + $writer->setHeader($header); + + // Attempt to pipe all items from the file to the database. + try { + $writer->writeItems($reader, -1); + } + catch (Exception $exception) { + throw $exception; + } + + // Report back with an array of status information. + return $writer->getReport(); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php new file mode 100644 index 0000000..c0dfc2f --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php @@ -0,0 +1,180 @@ +setOptions(array()); + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::getLangcode(). + */ + public function getLangcode() { + return $this->_langcode; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode(). + */ + public function setLangcode($langcode) { + $this->_langcode = $langcode; + } + + /** + * Get the options used by the reader. + */ + function getOptions() { + return $this->_options; + } + + /** + * Set the options for the current reader. + */ + function setOptions(array $options) { + $options += array( + 'customized' => FALSE, + 'not_customized' => FALSE, + 'not_translated' => FALSE, + ); + $this->_options = $options; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader(). + */ + function getHeader() { + return new PoHeader($this->getLangcode()); + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader(). + * + * @throws Exception + * Always, because you cannot set the PO header of a reader. + */ + function setHeader(PoHeader $header) { + throw new Exception('You cannot set the PO header in a reader.'); + } + + /** + * Builds and executes a database query based on options set earlier. + */ + private function buildQuery() { + $langcode = $this->_langcode; + $options = $this->_options; + + if (array_sum($options) == 0) { + // If user asked to not include anything in the translation files, + // that would not make sense, so just fall back on providing a template. + $langcode = NULL; + } + + // Build and execute query to collect source strings and translations. + $query = db_select('locales_source', 's'); + if (!empty($langcode)) { + if ($options['not_translated']) { + // Left join to keep untranslated strings in. + $query->leftJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $langcode)); + } + else { + // Inner join to filter for only translations. + $query->innerJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $langcode)); + } + if ($options['customized']) { + if (!$options['not_customized']) { + // Filter for customized strings only. + $query->condition('t.customized', LOCALE_CUSTOMIZED); + } + // Else no filtering needed in this case. + } + else { + if ($options['not_customized']) { + // Filter for non-customized strings only. + $query->condition('t.customized', LOCALE_NOT_CUSTOMIZED); + } + else { + // Filter for strings without translation. + $query->isNull('t.translation'); + } + } + $query->fields('t', array('translation')); + } + else { + $query->leftJoin('locales_target', 't', 's.lid = t.lid'); + } + $query->fields('s', array('lid', 'source', 'context', 'location')); + + $this->_result = $query->execute(); + } + + /** + * Get the database result resource for the given language and options. + */ + private function getResult() { + if (!isset($this->_result)) { + $this->buildQuery(); + } + return $this->_result; + } + + /** + * Implements Drupal\Component\Gettext\PoReaderInterface::readItem(). + */ + function readItem() { + $result = $this->getResult(); + $values = $result->fetchAssoc(); + if ($values) { + $poItem = new PoItem(); + $poItem->setFromArray($values); + return $poItem; + } + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php new file mode 100644 index 0000000..33f05d9 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php @@ -0,0 +1,306 @@ +setReport(); + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::getLangcode(). + */ + public function getLangcode() { + return $this->_langcode; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode(). + */ + public function setLangcode($langcode) { + $this->_langcode = $langcode; + } + + /** + * Get the report of the write operations. + */ + public function getReport() { + return $this->_report; + } + + /** + * Set the report array of write operations. + * + * @param array $report + * Associative array with result information. + */ + function setReport($report = array()) { + $report += array( + 'additions' => 0, + 'updates' => 0, + 'deletes' => 0, + 'skips' => 0, + ); + $this->_report = $report; + } + + /** + * Get the options used by the writer. + */ + function getOptions() { + return $this->_options; + } + + /** + * Set the options for the current writer. + */ + function setOptions(array $options) { + if (!isset($options['overwrite_options'])) { + $options['overwrite_options'] = array(); + } + $options['overwrite_options'] += array( + 'not_customized' => FALSE, + 'customized' => FALSE, + ); + $options += array( + 'customized' => LOCALE_NOT_CUSTOMIZED, + ); + $this->_options = $options; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader(). + */ + function getHeader() { + return $this->_header; + } + + /** + * Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader(). + * + * Sets the header and configure Drupal accordingly. + * + * Before being able to process the given header we need to know in what + * context this database write is done. For this the options must be set. + * + * A langcode is required to set the current header's PluralForm. + * + * @param Drupal\Component\Gettext\PoHeader $header + * Header metadata. + * + * @throws Exception + */ + function setHeader(PoHeader $header) { + $this->_header = $header; + $locale_plurals = variable_get('locale_translation_plurals', array()); + + // Check for options. + $options = $this->getOptions(); + if (empty($options)) { + throw new Exception("Options should be set before assigning a PoHeader."); + } + $overwrite_options = $options['overwrite_options']; + + // Check for langcode. + $langcode = $this->_langcode; + if (empty($langcode)) { + throw new Exception("Langcode should be set before assigning a PoHeader."); + } + + if (array_sum($overwrite_options) || empty($locale_plurals[$langcode]['plurals'])) { + // Get and store the plural formula if available. + $plural = $header->getPluralForms(); + if (isset($plural) && $p = $header->parsePluralForms($plural)) { + list($nplurals, $formula) = $p; + $locale_plurals[$langcode] = array( + 'plurals' => $nplurals, + 'formula' => $formula, + ); + variable_set('locale_translation_plurals', $locale_plurals); + } + } + } + + /** + * Implements Drupal\Component\Gettext\PoWriterInterface::writeItem(). + */ + function writeItem(PoItem $item) { + if ($item->isPlural()) { + $item->setSource(join(LOCALE_PLURAL_DELIMITER, $item->getSource())); + $item->setTranslation(join(LOCALE_PLURAL_DELIMITER, $item->getTranslation())); + } + $this->importString($item); + } + + /** + * Implements Drupal\Component\Gettext\PoWriterInterface::writeItems(). + */ + public function writeItems(PoReaderInterface $reader, $count = -1) { + $forever = $count == -1; + while (($count-- > 0 || $forever) && ($item = $reader->readItem())) { + $this->writeItem($item); + } + } + + /** + * Imports one string into the database. + * + * @param Drupal\Component\Gettext\PoItem $item + * The item being imported. + * + * @return int + * The string ID of the existing string modified or the new string added. + */ + private function importString(PoItem $item) { + // Initialize overwrite options if not set. + $this->_options['overwrite_options'] += array( + 'not_customized' => FALSE, + 'customized' => FALSE, + ); + $overwrite_options = $this->_options['overwrite_options']; + $customized = $this->_options['customized']; + + $context = $item->getContext(); + $source = $item->getSource(); + $translation = $item->getTranslation(); + + + // Look up the source string and any existing translation. + $string = db_query("SELECT s.lid, t.customized FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context", array( + ':source' => $source, + ':context' => $context, + ':language' => $this->_langcode, + )) + ->fetchObject(); + + if (!empty($translation)) { + // Skip this string unless it passes a check for dangerous code. + if (!locale_string_is_safe($translation)) { + watchdog('locale', 'Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $translation), WATCHDOG_ERROR); + $this->_report['skips']++; + return 0; + } + elseif (isset($string->lid)) { + if (!isset($string->customized)) { + // No translation in this language. + db_insert('locales_target') + ->fields(array( + 'lid' => $string->lid, + 'language' => $this->_langcode, + 'translation' => $translation, + 'customized' => $customized, + )) + ->execute(); + + $this->_report['additions']++; + } + elseif ($overwrite_options[$string->customized ? 'customized' : 'not_customized']) { + // Translation exists, only overwrite if instructed. + db_update('locales_target') + ->fields(array( + 'translation' => $translation, + 'customized' => $customized, + )) + ->condition('language', $this->_langcode) + ->condition('lid', $string->lid) + ->execute(); + + $this->_report['updates']++; + } + return $string->lid; + } + else { + // No such source string in the database yet. + $lid = db_insert('locales_source') + ->fields(array( + 'source' => $source, + 'context' => $context, + )) + ->execute(); + + db_insert('locales_target') + ->fields(array( + 'lid' => $lid, + 'language' => $this->_langcode, + 'translation' => $translation, + 'customized' => $customized, + )) + ->execute(); + + $this->_report['additions']++; + return $lid; + } + } + elseif (isset($string->lid) && isset($string->customized) && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) { + // Empty translation, remove existing if instructed. + db_delete('locales_target') + ->condition('language', $this->_langcode) + ->condition('lid', $string->lid) + ->execute(); + + $this->_report['deletes']++; + return $string->lid; + } + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php index 6fe77c2..016082a 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php @@ -197,6 +197,7 @@ class LocaleImportFunctionalTest extends WebTestBase { // Ensure the translation file was automatically imported when language was // added. $this->assertText(t('One translation file imported.'), t('Language file automatically imported.')); + $this->assertText(t('A translation string was skipped because of disallowed or malformed HTML'), t('Language file automatically imported.')); // Ensure strings were successfully imported. $search = array( @@ -206,6 +207,27 @@ class LocaleImportFunctionalTest extends WebTestBase { ); $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); $this->assertNoText(t('No strings available.'), t('String successfully imported.')); + + // Ensure multiline string was imported. + $search = array( + 'string' => 'Source string for multiline translation', + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText('Multiline translation string to make sure that import works with it.', t('String successfully imported.')); + + // Ensure 'Allowed HTML source string' was imported but the translation for + // 'Another allowed HTML source string' was not because it contains invalid + // HTML. + $search = array( + 'string' => 'HTML source string', + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText('Allowed HTML source string', t('String successfully imported.')); + $this->assertNoText('Another allowed HTML source string', t('String with disallowed translation not imported.')); } /** diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index f795f99..d9480a7 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -5,7 +5,10 @@ * Mass import-export and batch import functionality for Gettext .po files. */ -include_once DRUPAL_ROOT . '/core/includes/gettext.inc'; +use Drupal\Component\Gettext\PoStreamWriter; +use Drupal\locale\Gettext; +use Drupal\locale\PoDatabaseReader; + /** * User interface for the translation import screen. @@ -107,7 +110,39 @@ function locale_translate_import_form_submit($form, &$form_state) { $customized = $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED; // Now import strings into the language - if ($return = _locale_import_po($file, $language->langcode, $form_state['values']['overwrite_options'], $customized) == FALSE) { + try { + // Try to allocate enough time to parse and import the data. + drupal_set_time_limit(240); + + $report = GetText::fileToDatabase($file, $language->langcode, $form_state['values']['overwrite_options'], $customized); + $additions = $report['additions']; + $updates = $report['updates']; + $deletes = $report['deletes']; + $skips = $report['skips']; + + menu_router_rebuild(); + // Clear cache and force refresh of JavaScript translations. + _locale_invalidate_js($language->langcode); + cache()->deletePrefix('locale:'); + + 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' => $language->langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes)); + if ($skips) { + if (module_exists('dblog')) { + $skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => url('admin/reports/dblog'))); + } + else { + $skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.'); + } + drupal_set_message($skip_message, 'error'); + watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING); + } + $variables = array('%filename' => $file->filename); + drupal_set_message(t('The translation import of %filename is done.', $variables)); + watchdog('locale', 'The translation import of %filename is done.', $variables); + + } catch (Exception $exc) { + drupal_set_message(print_r($exc, TRUE)); $variables = array('%filename' => $file->filename); drupal_set_message(t('The translation import of %filename failed.', $variables), 'error'); watchdog('locale', 'The translation import of %filename failed.', $variables, WATCHDOG_ERROR); @@ -207,7 +242,42 @@ function locale_translate_export_form_submit($form, &$form_state) { $language = NULL; } $content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array(); - _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $content_options))); + $reader = new PoDatabaseReader(); + $languageName = ''; + if ($language != NULL) { + $reader->setLangcode($language->langcode); + $reader->setOptions($content_options); + $languages = language_list(); + $languageName = isset($languages[$language->langcode]) ? $languages[$language->langcode]->name : ''; + $filename = $language->langcode .'.po'; + } else { + // Template required. + $filename = 'drupal.pot'; + } + $item = $reader->readItem(); + if (!empty($item)) { + $uri = tempnam('temporary://', 'po_'); + $header = $reader->getHeader(); + $header->setProjectName(variable_get('site_name', 'Drupal')); + $header->setLanguageName($languageName); + + $writer = new PoStreamWriter; + $writer->setUri($uri); + $writer->setHeader($header); + + $writer->open(); + $writer->writeItem($item); + $writer->writeItems($reader); + $writer->close(); + + header("Content-Disposition: attachment; filename=$filename"); + header("Content-Type: text/plain; charset=utf-8"); + print file_get_contents($uri); + drupal_exit(); + } + else { + drupal_set_message('Nothing to export.'); + } } /** @@ -318,22 +388,32 @@ function locale_translate_batch_build($files, $finish_feedback = FALSE) { /** * Perform interface translation import as a batch step. * + * The given filepath is matched against ending with '{langcode}.po'. When + * matched the filepath is added to batch context. + * * @param $filepath * Path to a file to import. - * @param $results + * @param $context * Contains a list of files imported. */ function locale_translate_batch_import($filepath, &$context) { // The filename is either {langcode}.po or {prefix}.{langcode}.po, so // we can extract the language code to use for the import from the end. if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) { - $file = locale_translate_file_create($filepath, $langcode[2]); - $success = _locale_import_read_po('db-store', $file, array(), $langcode[2]); - if ($success == NULL) { - $file->langcode = $langcode[2]; + $file = entity_create('file', array('filename' => drupal_basename($filepath), 'uri' => $filepath)); + // We need only the last match + $langcode = array_pop($langcode); + try { + $report = GetText::fileToDatabase($file, $langcode, array(), LOCALE_NOT_CUSTOMIZED); + $file->langcode = $langcode; + $file->timestamp = filemtime($file->uri); locale_translate_update_file_history($file); + $context['results']['files'][$filepath] = $filepath; + $context['results']['stats'][$filepath] = $report; + } catch (Exception $exception) { + $context['results']['files'][$filepath] = $filepath; + $context['results']['failed_files'][$filepath] = $filepath; } - $context['results'][] = $filepath; } } @@ -342,7 +422,30 @@ function locale_translate_batch_import($filepath, &$context) { */ function locale_translate_batch_finished($success, $results) { if ($success) { - drupal_set_message(format_plural(count($results), 'One translation file imported.', '@count translation files imported.')); + $additions = $updates = $deletes = $skips = 0; + drupal_set_message(format_plural(count($results['files']), 'One translation file imported.', '@count translation files imported.')); + $skipped_files = array(); + foreach ($results['stats'] as $filepath => $report) { + $additions += $report['additions']; + $updates += $report['updates']; + $deletes += $report['deletes']; + $skips += $report['skips']; + if ($report['skips'] > 0) { + $skipped_files[] = $filepath; + } + } + 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', 'The translation was succesfully imported. %number new strings added, %update updated and %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)); + if ($skips) { + if (module_exists('dblog')) { + $skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => url('admin/reports/dblog'))); + } + else { + $skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.'); + } + drupal_set_message($skip_message, 'error'); + watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING); + } } } diff --git a/core/modules/locale/tests/test.xx.po b/core/modules/locale/tests/test.xx.po index 659a6e3..e2db1d3 100644 --- a/core/modules/locale/tests/test.xx.po +++ b/core/modules/locale/tests/test.xx.po @@ -26,3 +26,15 @@ msgstr "samedi" msgid "Sunday" msgstr "dimanche" + +msgid "Allowed HTML source string" +msgstr "Allowed HTML translation string" + +msgid "Another allowed HTML source string" +msgstr "" + +msgid "Source string for multiline translation" +msgstr "" +"Multiline translation string " +"to make sure that " +"import works with it."