diff --git a/core/includes/gettext.inc b/core/includes/gettext.inc deleted file mode 100644 index 4e3b221..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 ' . variable_get('site_name', 'Drupal') . "\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 96e84b3..fbd279e 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -2,6 +2,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Config\FileStorage; +use Drupal\Core\Gettext\Gettext; /** * Indicates that a module has not been installed yet. @@ -717,13 +718,7 @@ function st($string, array $args = array(), array $options = array()) { // that multiple files end with the same extension, even if unlikely. $files = install_find_translation_files($install_state['parameters']['langcode']); if (!empty($files)) { - // Include cross-dependent code from locale module and gettext.inc. - require_once DRUPAL_ROOT . '/core/modules/locale/locale.module'; - require_once DRUPAL_ROOT . '/core/includes/gettext.inc'; - foreach ($files as $file) { - _locale_import_read_po('mem-store', $file); - } - $strings = _locale_import_one_string('mem-report'); + $strings = Gettext::filesToArray($install_state['parameters']['langcode'], $files); } } } diff --git a/core/lib/Drupal/Component/Gettext/PoFileReader.php b/core/lib/Drupal/Component/Gettext/PoFileReader.php new file mode 100644 index 0000000..1948e7e --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoFileReader.php @@ -0,0 +1,549 @@ +_uri; + } + + /** + * Set the URI of the po file. + */ + public function setURI($uri) { + $this->_uri = $uri; + } + + /** + * Get the language code of the po file. + */ + public function getLangcode() { + return $this->_langcode; + } + + /** + * Set the language code of the po file. + */ + public function setLangcode($langcode) { + $this->_langcode = $langcode; + } + + /** + * Get the header of the po file. + */ + public function getHeader() { + return $this->_header; + } + + /** + * Set the header of the po file. + */ + public function setHeader(PoHeader $header) { + // This needs to be implemented based on the interface, but isn't used. + } + + /** + * Open the po file and read the header. + */ + 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 without URI set'); + } + } + + /** + * Close the po file. + */ + public function close() { + if ($this->_fd) { + fclose($this->_fd); + } + } + + /** + * Return a translation object (singular or plural). + */ + public function readItem() { + $this->readTranslation(); + return $this->translation; + } + + /** + * Read translation object from the po file. + */ + private function readTranslation() { + $this->translation = NULL; + while (!$this->finished && is_null($this->translation)) { + $this->readLine(); + } + return $this->translation; + } + + /** + * Read the header from the po file. + */ + private function readHeader() { + $translation = $this->readTranslation(); + $header = new PoHeader; + $header->setFromString(trim($translation->translation)); + $this->_header = $header; + } + + /** + * Reads a line from a PO file. + * + * While reading a line it's content is processed according to current + * context. + * + * The parser context. Can be: + * - 'COMMENT' (#) + * - 'MSGID' (msgid) + * - 'MSGID_PLURAL' (msgid_plural) + * - 'MSGCTXT' (msgctxt) + * - 'MSGSTR' (msgstr or msgstr[]) + * - 'MSGSTR_ARR' (msgstr_arg) + * + * @return boolean FALSE or NULL + */ + private function readLine() { + // a string or boolean FALSE + $line = fgets($this->_fd); + $this->finished = ($line === FALSE); + if (!$this->finished) { + + if ($this->lineno == 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->plural = 0; + } + + $this->lineno++; + + // Trim away the linefeed. + $line = trim(strtr($line, array("\\\n" => ""))); + + if (!strncmp('#', $line, 1)) { + // Lines starting with '#' are comments. + + if ($this->context == 'COMMENT') { + // Already in comment token, insert the comment. + $this->current['#'][] = substr($line, 1); + } + elseif (($this->context == 'MSGSTR') || ($this->context == 'MSGSTR_ARR')) { + // We are currently in string token, close it out. + $this->saveOneString(); + + // Start a new entry for the comment. + $this->current = array(); + $this->current['#'][] = substr($line, 1); + + $this->context = 'COMMENT'; + return TRUE; + } + else { + // A comment following any other token is a syntax error. + $this->log('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $this->lineno); + return FALSE; + } + return; + } + elseif (!strncmp('msgid_plural', $line, 12)) { + // A plural form for the current message. + + if ($this->context != 'MSGID') { + // A plural form cannot be added to anything else but the id directly. + $this->log('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $this->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 = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The plural form must be wrapped in quotes. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + // Append the plural form to the current entry. + if (is_string($this->current['msgid'])) { + // The first value was stored as string. Now we know the context is + // plural, it is converted to array. + $this->current['msgid'] = array($this->current['msgid']); + } + $this->current['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 a message string, close it out. + $this->saveOneString(); + + // Start a new context for the id. + $this->current = array(); + } + elseif ($this->context == 'MSGID') { + // We are currently already in the context, meaning we passed an id with no data. + $this->log('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $this->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 = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The message id must be wrapped in quotes. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + $this->current['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 a message, start a new one. + $this->saveOneString($this->current); + $this->current = array(); + } + elseif (!empty($this->current['msgctxt'])) { + // A context cannot apply to another context. + $this->log('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $this->lineno); + return FALSE; + } + + // Remove 'msgctxt' and trim away whitespaces. + $line = trim(substr($line, 7)); + // At this point, $line should now contain the context. + + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The context string must be quoted. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + $this->current['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')) { + // Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries. + $this->log('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $this->lineno); + return FALSE; + } + + // Ensure the plurality is terminated. + if (strpos($line, ']') === FALSE) { + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + // Extract the plurality. + $frombracket = strstr($line, '['); + $this->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 = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + if (!isset($this->current['msgstr']) || !is_array($this->current['msgstr'])) { + $this->current['msgstr'] = array(); + } + + $this->current['msgstr'][$this->plural] = $quoted; + + $this->context = 'MSGSTR_ARR'; + return; + } + elseif (!strncmp("msgstr", $line, 6)) { + // A string for the an id or context. + + if (($this->context != 'MSGID') && ($this->context != 'MSGCTXT')) { + // Strings are only valid within an id or context scope. + $this->log('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $this->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 = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + $this->current['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) { + // The string must be quoted. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + // Append the string to the current context. + if (($this->context == 'MSGID') || ($this->context == 'MSGID_PLURAL')) { + if (is_array($this->current['msgid'])) { + // Add string to last array element. + $last_index = count($this->current['msgid']) - 1; + $this->current['msgid'][$last_index] .= $quoted; + } + else { + $this->current['msgid'] .= $quoted; + } + } + elseif ($this->context == 'MSGCTXT') { + $this->current['msgctxt'] .= $quoted; + } + elseif ($this->context == 'MSGSTR') { + $this->current['msgstr'] .= $quoted; + } + elseif ($this->context == 'MSGSTR_ARR') { + $this->current['msgstr'][$this->plural] .= $quoted; + } + else { + // No valid context to append to. + $this->log('The translation file %filename contains an error: there is an unexpected string on line %line.', $this->lineno); + return FALSE; + } + return; + } + } + + // Empty line read or EOF of PO file, closed out the last entry. + if (($this->context == 'MSGSTR') || ($this->context == 'MSGSTR_ARR')) { + $this->saveOneString($this->current); + $this->current = array(); + } + elseif ($this->context != 'COMMENT') { + $this->log('The translation file %filename ended unexpectedly at line %line.', $this->lineno); + return FALSE; + } + } + + /** + * Sets an error message if an error occurred during locale file parsing. + * + * @param $message + * The message to be translated. + * @param $lineno + * An optional line number argument. + */ + protected function log($message, $lineno = NULL) { + if (isset($lineno)) { + $vars['%line'] = $lineno; + } + $t = get_t(); + $this->errorLog[] = $t($message, $vars); + } + + /** + * Store the parsed values as translation object. + */ + public function saveOneString() { + $value = $this->current; + $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; + } + + $translation = new PoItem; + $translation->context = isset($value['msgctxt']) ? $value['msgctxt'] : ''; + $translation->source = $value['msgid']; + $translation->translation = $value['msgstr']; + $translation->plural = $plural; + $translation->comment = $comments; + $translation->langcode = $this->getLangcode(); + + $this->translation = $translation; + + $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)) { + 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 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/PoFileWriter.php b/core/lib/Drupal/Component/Gettext/PoFileWriter.php new file mode 100644 index 0000000..ff877d3 --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoFileWriter.php @@ -0,0 +1,139 @@ +_header; + } + + /** + * Set the po header for the current file. + */ + public function setHeader(PoHeader $header) { + $this->_header = $header; + } + + /** + * Get the current language used. + */ + public function getLangcode() { + return $this->_langcode; + } + + /** + * Set the language. + */ + public function setLangcode($langcode) { + $this->_langcode = $langcode; + } + + /** + * Open the file and start write the header if needed. + */ + public function open() { + // Open in append mode + $this->_fd = fopen($this->getURI(), 'a'); + $this->_seekpos = ftell($this->_fd); + if ($this->_seekpos == 0) { + $this->writeHeader(); + } + } + + /** + * Close the file. + */ + public function close() { + fclose($this->_fd); + } + + /** + * Write data to the file. + */ + 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 file. + */ + private function writeHeader() { + $this->write($this->_header); + } + + /** + * Write one PoItem to the file. + */ + public function writeItem(PoItem $item) { + $this->write($item); + } + + /** + * Write one or more PoItem's to the file. + */ + public function writeItems(PoReaderInterface $reader, $count = -1) { + $forever = $count == -1; + while (($count-- > 0 || $forever) && ($item = $reader->readItem())) { + $this->writeItem($item); + } + } + + /** + * Gets the URI of the current file. + */ + public function getURI() { + if (empty($this->_uri)) { + throw new Exception('Empty URI'); + } + return $this->_uri; + } + + /** + * Set the URI to be used. + */ + public function setURI($uri) { + $this->_uri = $uri; + } + +} diff --git a/core/lib/Drupal/Component/Gettext/PoHeader.php b/core/lib/Drupal/Component/Gettext/PoHeader.php new file mode 100644 index 0000000..79e29c1 --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoHeader.php @@ -0,0 +1,422 @@ +1);\n" + */ +class PoHeader { + + /** + * Language code. + */ + private $_langcode; + + /** + * Formula for the plural form. + */ + private $_pluralForms; + + /** + * Author(s) of the file. + */ + private $_authors; + + /** + * Date the po file got created. + */ + private $_po_date; + + /** + * Human readable language name. + */ + private $_languageName; + + /** + * Name of the project the translation belongs to. + */ + private $_projectName; + + /** + * Creates a PoHeader with default values set. + * + * @param string $langcode + * @see setDefaults() + */ + public function __construct($langcode = NULL) { + $this->_langcode = $langcode; + $this->_po_date = date("Y-m-d H:iO"); + $this->_pluralForms = 'nplurals=2; plural=(n > 1);'; + $this->setDefaults(); + } + + /** + * Mapping between po file and class. + */ + static public function mapping() { + return array( + 'POT-Creation-Date' => '_potCreationDate', + 'Plural-Forms' => '_pluralForms', + ); + } + + /** + * Get the plural form. + */ + function getPlural() { + return $this->_pluralForms; + } + + /** + * Set the human readable language name. + */ + function setLanguageName($languageName) { + $this->_languageName = $languageName; + } + + /** + * Get the human readable language name. + */ + function getLanguageName() { + return $this->_languageName; + } + + /** + * Set the project name. + */ + function setProjectName($projectName) { + $this->_projectName = $projectName; + } + + /** + * Get the project name. + */ + function getProjectName() { + return $this->_projectName; + } + + /** + * Compile the PO header. + */ + private function compileHeader() { + $output = ''; + + $isTemplate = $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; + } + + /** + * Store a given PO Header string. + * + * @param string $header + * @see mapping() + */ + public function setFromString($header) { + $values = $this->parseHeader($header); + $this->setDefaults($values); + } + + /** + * Set defaults + * + * @param array $values + * @see mapping() + */ + public function setDefaults($values = array()) { + $mapping = self::mapping(); + foreach ($mapping as $key => $var) { + if (isset($values[$key])) { + $this->{$var} = $values[$key]; + } + } + } + + public function __toString() { + $result = $this->compileHeader() . "\n"; + return $result; + } + + /** + * 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 PHP formula and number of plural forms. + */ + function parsePluralForms($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 = $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 $header + * A string containing the complete header. + * + * @return + * 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 + * A string containing the arithmetic formula. + * + * @return + * The PHP version of the formula. + */ + 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 + * A string containing the arithmetic formula. + * + * @return + * The PHP version of 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/PoInterface.php b/core/lib/Drupal/Component/Gettext/PoInterface.php new file mode 100644 index 0000000..90b7d84 --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoInterface.php @@ -0,0 +1,25 @@ + $value) { + $this->{$key} = $value; + } + if (isset($this->source) && strpos($this->source, LOCALE_PLURAL_DELIMITER) !== FALSE) { + $this->source = explode(LOCALE_PLURAL_DELIMITER, $this->source); + $this->translation = explode(LOCALE_PLURAL_DELIMITER, $this->translation); + $this->plural = count($this->translation); + } + } + + /** + * 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..cb54b12 --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoMemoryWriter.php @@ -0,0 +1,65 @@ +_items = array(); + } + + /** + * Store a PoItem into memory. + * + * @param PoItem $item + */ + 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; + } + + /** + * Store one or more PoItem's into memory. + * + * @param PoItem $item + * @param int $count + */ + 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; + } +} diff --git a/core/lib/Drupal/Component/Gettext/PoReaderInterface.php b/core/lib/Drupal/Component/Gettext/PoReaderInterface.php new file mode 100644 index 0000000..97a99e8 --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/PoReaderInterface.php @@ -0,0 +1,17 @@ +setLangcode($langcode); + foreach ($files as $file) { + $reader = new PoFileReader(); + $reader->setURI($file->uri); + $reader->setLangcode($langcode); + $reader->open(); + $writer->writeItems($reader, -1); + } + return $writer->getData(); + } + + /** + * Reads the given PO files into a data structure. + * + * @param file $file + * @param string $langcode + * @param array $overwrite_options + * @param boolean $customized + * @return array + */ + static function fileToDatabase($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) { + $reader = new PoFileReader(); + $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.'); + } + + $writer = new PoDatabaseWriter(); + $writer->setLangcode($langcode); + $options = array( + 'overwrite_options' => $overwrite_options, + 'customized' => $customized, + ); + // It's vital options are set first. + // @todo: this has to be fixed in https://drupal.org/node/1637334 + $writer->setOptions($options); + $writer->setHeader($header); + + try { + $writer->writeItems($reader, -1); + } + catch (Exception $exception) { + throw $exception; + } + + 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..7d9ddfa --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php @@ -0,0 +1,177 @@ +setOptions(array()); + } + + /** + * Get the language code of the currently used language. + */ + public function getLangcode() { + return $this->_langcode; + } + + /** + * Set the language code of the current language. + */ + 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['overwrite_options'] += array( + 'not_customized' => FALSE, + 'customized' => FALSE, + ); + $options += array( + 'customized' => LOCALE_NOT_CUSTOMIZED, + ); + $this->_options = $options; + } + + /** + * Get the header in po format based on the current language code. + */ + function getHeader() { + return new PoHeader($this->getLangcode()); + } + + /** + * Set the header in PO format in a reader is not allowed. + * + * @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.'); + } + + /** + * Generates a structured array of all translated strings for the language. + * + * @return + * An array of translated strings that can be used to generate an export. + */ + private function buildQuery() { + $langcode = $this->_langcode; + $options = $this->_options; + + // 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. + $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 full result for the given language and options. + * + * @see buildQuery + */ + private function getResult() { + if (!isset($this->_result)) { + $this->buildQuery(); + } + return $this->_result; + } + + /** + * Read one item at the time from the results. + * + * @see PoDatabaseWriter + */ + 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..bbe8178 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php @@ -0,0 +1,337 @@ +setState(array()); + } + + /** + * Get the language code of the currently used language. + */ + public function getLangcode() { + return $this->_langcode; + } + + /** + * Set the language code of the current language. + */ + public function setLangcode($langcode) { + $this->_langcode = $langcode; + } + + /** + * Get the report of the write operations. + */ + public function getReport() { + return $this->_report; + } + + /** + * Set the report of the write operations. + */ + function setReport($report) { + $report += array( + 'additions' => 0, + 'updates' => 0, + 'deletes' => 0, + 'skips' => 0, + 'ignored' => 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) { + $options['overwrite_options'] += array( + 'not_customized' => FALSE, + 'customized' => FALSE, + ); + $options += array( + 'customized' => LOCALE_NOT_CUSTOMIZED, + ); + $this->_options = $options; + } + + /** + * Get the header. + * @return PoHeader + * @see setHeader + */ + function getHeader() { + return $this->_header; + } + + /** + * 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 headers PluralForm. + * + * @param PoHeader $header + * @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. + $lang = $this->_langcode; + if (empty($lang)) { + throw new Exception("Langcode should be set before assigning a PoHeader"); + } + + if (array_sum($overwrite_options) || empty($locale_plurals[$lang]['plurals'])) { + // Get and store the plural formula if available. + $plural = $header->getPlural(); + $filepath = __CLASS__ . "::" . __METHOD__; + if (isset($plural) && $p = $header->parsePluralForms($plural, $filepath)) { + list($nplurals, $formula) = $p; + $locale_plurals[$lang] = array( + 'plurals' => $nplurals, + 'formula' => $formula, + ); + variable_set('locale_translation_plurals', $locale_plurals); + } + } + } + + /** + * Write a PoItem to the database. + * + * @param PoItem $item + */ + function writeItem(PoItem $item) { + if ($item->plural) { + $item->source = join(LOCALE_PLURAL_DELIMITER, $item->source); + $item->translation = join(LOCALE_PLURAL_DELIMITER, $item->translation); + } + $this->importString($item, 'location'); + } + + /** + * Write one or more PoItems to the database. + * + * @param PoReaderInterface $reader + * Reader to read PoItem's from + * @param $count + * When $count == -1 all items are read from the $reader. + */ + 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 PoItem $item + * The item being imported. + * @param $location + * Location value to save with source string. + * + * @return + * The string ID of the existing string modified or the new string added. + */ + private function importString($item, $location) { + // 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']; + + // 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' => $item->source, + ':context' => $item->context, + ':language' => $this->_langcode, + )) + ->fetchObject(); + + if (!empty($item->translation)) { + // Skip this string unless it passes a check for dangerous code. + if (!locale_string_is_safe($item->translation)) { + watchdog('locale', 'Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $item->translation), WATCHDOG_ERROR); + $this->_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' => $this->_langcode, + 'translation' => $item->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' => $item->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( + 'location' => $location, + 'source' => $item->source, + 'context' => (string) $item->context, + )) + ->execute(); + + db_insert('locales_target') + ->fields(array( + 'lid' => $lid, + 'language' => $this->_langcode, + 'translation' => $item->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; + } + } + + /** + * Default state for the database writer. + */ + static function getDefaultState() { + return array( + 'langcode' => NULL, + 'report' => array( + 'additions' => 0, + 'updates' => 0, + 'deletes' => 0, + 'skips' => 0, + 'ignored' => 0, + ), + 'options' => array( + 'overwrite_options' => array( + 'not_customized' => FALSE, + 'customized' => FALSE, + ), + 'customized' => LOCALE_NOT_CUSTOMIZED, + ), + ); + } + + /** + * Set the state for in between file batch API. + * + * @param array $state + */ + public function setState(array $state) { + $state += self::getDefaultState(); + $this->_report = $state['report']; + $this->setLangcode($state['langcode']); + $this->setOptions($state['options']); + } + + /** + * Get the state for in between file batch API. + * + * @return array $state + */ + public function getState() { + return array( + 'class' => __CLASS__, + 'report' => $this->getReport(), + 'langcode' => $this->getLangcode(), + 'options' => $this->getOptions(), + ); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php index 6fe77c2..e55f356 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php @@ -196,7 +196,8 @@ 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('3 translation files 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,25 @@ 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' => 'HTTP Result Code: !status', + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), t('String successfully imported.')); + + // Ensure xyzzy was imported and xyzzy not. + $search = array( + 'string' => 'xyzzy', + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), t('String successfully imported.')); + $this->assertNoText('xyzzy2', t('String successfully imported.')); } /** diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 7a8a977..ab78bf7 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\PoFileWriter; +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 PoFileWriter; + $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.'); + } } /** @@ -288,9 +358,12 @@ 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) { @@ -298,8 +371,16 @@ function locale_translate_batch_import($filepath, &$context) { // we can extract the language code to use for the import from the end. if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) { $file = entity_create('file', array('filename' => drupal_basename($filepath), 'uri' => $filepath)); - _locale_import_read_po('db-store', $file, array(), $langcode[2]); - $context['results'][] = $filepath; + // We need only the last match + $langcode = array_pop($langcode); + try { + $report = GetText::fileToDatabase($file, $langcode, array(), LOCALE_NOT_CUSTOMIZED); + $context['results']['files'][$filepath] = $filepath; + $context['results']['stats'][$filepath] = $report; + } catch (Exception $exc) { + $context['results']['files'][$filepath] = $filepath; + $context['results']['failed_files'][$filepath] = $filepath; + } } } @@ -308,6 +389,29 @@ 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/test2.xx.po b/core/modules/locale/tests/test2.xx.po new file mode 100755 index 0000000..1733f69 --- /dev/null +++ b/core/modules/locale/tests/test2.xx.po @@ -0,0 +1,15 @@ +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "xyzzy" +msgstr "html" + +msgid "xyzzy2" +msgstr "" + + diff --git a/core/modules/locale/tests/test3.xx.po b/core/modules/locale/tests/test3.xx.po new file mode 100755 index 0000000..3840a0a --- /dev/null +++ b/core/modules/locale/tests/test3.xx.po @@ -0,0 +1,16 @@ +msgid "" +msgstr "" +"Project-Id-Version: Drupal 7\\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\\n" + +msgid "HTTP Result Code: !status" +msgstr "" +"a multiline test" +"second\r\n" +"third" + + +