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/gettext.sketch.inc b/core/includes/gettext.sketch.inc
new file mode 100644
index 0000000..fdaa2c5
--- /dev/null
+++ b/core/includes/gettext.sketch.inc
@@ -0,0 +1,195 @@
+setURI('public://source.po.txt');
+$reader->setLangcode($langcode);
+$reader->open();
+
+// Get header
+$header = $reader->getHeader();
+
+$writer = new PoDatabaseWriter();
+$writer->setLangcode($langcode);
+
+// Write 10 items
+$writer->writeItems($reader, 10);
+$state = $reader->getState();
+// Store state in Batch
+$reader->setState($state);
+
+// Write remaining items
+$writer->writeItems($reader);
+
+// Invoke it through batch
+gettext_transfer($reader, $writer);
+
+/**
+ * Example code to export translations to a po file.
+ *
+ * Export gettext data to a PO file.
+ * The file may be local, remote (via stream wrapper) or even XML.
+ * The import has a small memory footprint.
+ * The import may be split up to enable batch handling.
+ */
+
+$reader = new PoDatabaseReader;
+$reader->setLangcode($langcode);
+$destination = 'public://destination.po.txt';
+$writer = new PoFileWriter();
+$writer->setURI($destination);
+$writer->setLangcode($langcode);
+$writer->setHeader($reader->getHeader());
+
+
+$writer->writeItems($reader);
+
+/**
+ * Transfers gettext data from source to destination.
+ */
+function gettext_transfer($reader, $writer) {
+ // Use batch processing if both source and destination support it and the source
+ // is large enough. If not, process in once.
+ if ($reader instanceof BatchStateInterface && $writer instanceof BatchStateInterface) {
+ // Transfer data in batches.
+ // Built and execute batch.
+ $batch = gettext_transfer_batch_setup($reader, $writer);
+ batch_set($batch);
+ }
+ else {
+ $writer->writeItems($reader);
+ }
+}
+
+/**
+ * Set up a batch process to transfer Gettext data.
+ */
+function gettext_transfer_batch_setup($reader, $writer) {
+ $batch = array(
+ 'operations' => array(
+ // TODO: can this be objects?
+ // Shouldn't we use $reader->getState() / $writer->getState();
+ array('gettext_transfer_batch_op', array($reader, $writer)),
+ ),
+ 'finished' => 'gettext_transfer_batch_finished',
+ );
+ return $batch;
+}
+
+/**
+ * Batch operation transferring gettext data.
+ *
+ * @todo Add source filter conditions (context, language(?)) as parameter.
+ */
+function gettext_transfer_batch_op($source, $destination) {
+ // Initialize sandbox for batch reading.
+ if (empty($context['sandbox'])) {
+ $context['sandbox']['transfer'] = array(
+ 'chunk' => min($source->chunkSize(), $destination->chunkSize()),
+ );
+ }
+
+ // Execute one transfer cycle, which will process bite size 'chunks'.
+ $success = gettext_transfer_execute($source, $destination, $context['sandbox']['transfer']);
+
+ // After a number of cycles executing gettext_transfer_execute() the transfer
+ // is completed.
+ // See gettextapi_import_batch_op() for details.
+ if ($context['sandbox']['transfer']['finished']) {
+ $context['finished'] = 1;
+
+ if ($success) {
+ // Collect statistics from $context['sandbox']['transfer']
+ // and tell the world about what great work we did.
+ }
+ }
+}
+
+/**
+ * Batch wrap-up for gettext data transfer.
+ */
+function gettext_transfer_batch_finished($success, $results, $operations) {
+ if ($success) {
+ // Call post processing handler defined by the destination object.
+ // Can we get the callback and arguments via $results?
+ $post_process_callback = $results['post_process_callback'];
+ $arguments = $results['post_process_arguments'];
+ $post_process_callback($arguments);
+
+ // We did it, Magoo! :)
+ }
+ else {
+ // Sadly reporting where it went wrong.
+ }
+}
+
+/**
+ * Post processing for gettext language import into the database.
+ *
+ * To be called after successfull (batch) import.
+ */
+function gettext_post_process_import($langcode) {
+ // After succesfull import we refresh all affected parts of the system.
+ _locale_invalidate_js($langcode);
+ cache_clear_all('locale:', 'cache', TRUE);
+ menu_rebuild();
+}
+
+/**
+ * Transfer gettext data from source to destination in bite size chunks.
+ */
+function gettext_transfer_execute($source, $destination, &$transfer) {
+ // If transfer is not yet started we set the header data.
+ // The header (if supported) is written before the first translation is written.
+ if (!$destination->inProgress()) {
+ $destination->setHeader($source->getHeader());
+ }
+
+ //TODO: this is not yet rewriten to PoFile|Database|Reader|Writer
+ $chunk = $transfer['chunk'];
+ // Transfer translations as long as valid data is available and
+ // the bite size chunk is not yet swallowed.
+ while ($source->valid() && $chunk > 0) {
+ // Get one translation from source, write it to destination.
+ $translation = $source->read();
+ $destination->write($translation);
+ $chunk--;
+ }
+
+ // Report the percentage of completion for progress reporting.
+ // Ai, user interface. Why do we bother ;)
+ $transfer['state']['percentage_of_completion'] = $source->poc();
+
+ // Close connections when we are done and report the results.
+ if ($source->finished()) {
+ $destination->finish();
+ $transfer['finished'] = TRUE;
+ }
+
+ // Report statistics including errors and error log.
+ $transfer['result'] = $destination->statistics();
+}
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/BatchStateInterface.php b/core/lib/Drupal/Component/Gettext/BatchStateInterface.php
new file mode 100644
index 0000000..081ac24
--- /dev/null
+++ b/core/lib/Drupal/Component/Gettext/BatchStateInterface.php
@@ -0,0 +1,51 @@
+
+ * class MyReader implements BatchStateInterface {
+ * function __construct(){
+ * // empty
+ * }
+ * ...
+ * function getState() {
+ * return array(
+ * '__CLASS__' => __CLASS__,
+ * 'my_key' => 'my value',
+ * );
+ * }
+ * }
+ *
+ */
+interface BatchStateInterface {
+
+ /**
+ * Returns the current state used for resetting state later on.
+ *
+ * The state is used to reconstruct the state of the object by calling
+ * setState().
+ *
+ * The Class implemeting this interface must have an empty constructor.
+ *
+ * @return array
+ * key/value pairs of which one must be __CLASS__
+ */
+ function getState();
+
+ /**
+ * Sets the object ready to roll.
+ *
+ * After calling setState it is assumed the object is ready to do it's work.
+ */
+ function setState(array $state);
+}
diff --git a/core/lib/Drupal/Component/Gettext/PoFileReader.php b/core/lib/Drupal/Component/Gettext/PoFileReader.php
new file mode 100644
index 0000000..a6d972f
--- /dev/null
+++ b/core/lib/Drupal/Component/Gettext/PoFileReader.php
@@ -0,0 +1,556 @@
+_uri;
+ }
+
+ public function setURI($uri) {
+ $this->_uri = $uri;
+ }
+
+ public function getLangcode() {
+ return $this->_langcode;
+ }
+
+ public function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+
+ public function open() {
+ if (!empty($this->_uri)) {
+ $this->_fd = fopen($this->_uri, 'rb');
+ $this->_size = ftell($this->_fd);
+ // We immediately read the header as we are at BOF
+ $this->readHeader();
+ }
+ else {
+ throw new \Exception("Cannot open without URI set");
+ }
+ }
+
+ public function close() {
+ if ($this->_fd) {
+ fclose($this->_fd);
+ }
+ }
+
+
+ public function setState(array $state) {
+ $this->setURI($state['uri']);
+ $this->setLangcode($state['langcode']);
+ // Make sure to (re)read the PoHeader
+ $this->open();
+ // Move to last read position.
+ if (isset($state['seekpos'])) {
+ fseek($this->_fd, $state['seekpos']);
+ }
+ if (isset($state['lineno'])) {
+ $this->lineno = $state['lineno'];
+ }
+ }
+
+ public function getState() {
+ return array(
+ 'class' => __CLASS__,
+ 'uri' => $this->_uri,
+ 'langcode' => $this->_langcode,
+ 'seekpos' => ftell($this->_fd),
+ 'lineno' => $this->lineno,
+ );
+ }
+
+ /**
+ * Return a translation object (singular or plural)
+ *
+ * @todo Define a translation object for this purpose?
+ * Or use a standard class for better performance?
+ */
+ public function readItem() {
+ $this->readTranslation();
+ return $this->translation;
+ }
+
+ private function readTranslation() {
+ $this->translation = NULL;
+ while (!$this->finished && is_null($this->translation)) {
+ $this->readLine();
+ }
+ return $this->translation;
+ }
+
+ public function getHeader() {
+ return $this->_header;
+ }
+
+ public function setHeader(PoHeader $header) {
+ // TODO : throw exception?
+ }
+
+ /**
+ * Reads the header from the given input stream.
+ *
+ * We need to read the optional first COMMENT
+ * Next read a MSGID and a MSGSTR
+ */
+ 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..182fcb8
--- /dev/null
+++ b/core/lib/Drupal/Component/Gettext/PoFileWriter.php
@@ -0,0 +1,112 @@
+_header;
+ }
+
+ public function setHeader(PoHeader $header) {
+ $this->_header = $header;
+ }
+
+ public function getLangcode() {
+ return $this->_langcode;
+ }
+
+ public function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+ public function open() {
+ // Open in append mode
+ $this->_fd = fopen($this->getURI(), 'a');
+ $this->_seekpos = ftell($this->_fd);
+ if ($this->_seekpos == 0) {
+ // If file is new position == 0
+ $this->writeHeader();
+ }
+ else {
+ $reader = new PoFileReader($this->uri);
+ $this->_header = $reader->getHeader();
+ }
+ }
+
+ public function close() {
+ fclose($this->_fd);
+ }
+
+ public function setState(array $state) {
+ $this->_uri = $state['uri'];
+ $this->open();
+ }
+
+ public function getState() {
+ return array(
+ 'uri' => $this->_uri,
+ 'seekpos' => ftell($this->_fd),
+ );
+ }
+
+ private function write($data) {
+ $result = fputs($this->_fd, $data);
+ if ($result === FALSE) {
+ // TODO: better context for message
+ throw new \Exception("Unable to write data : " . substr($data, 0, 20));
+ }
+ $this->_seekpos = ftell($this->_fd);
+ }
+
+ private function writeHeader() {
+ $this->write($this->_header);
+ }
+
+ public function writeItem(PoItem $item) {
+ $this->write($item);
+ }
+
+ public function writeItems(PoReaderInterface $reader, $count = -1) {
+ $forever = $count == -1;
+ while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
+ $this->writeItem($item);
+ }
+ }
+
+ public function getURI() {
+ if (empty($this->_uri)) {
+ throw new \Exception("Empty URI");
+ }
+ return $this->_uri;
+ }
+
+ 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..724f108
--- /dev/null
+++ b/core/lib/Drupal/Component/Gettext/PoHeader.php
@@ -0,0 +1,405 @@
+1);\n"
+
+ * @author clemens
+ */
+class PoHeader {
+
+ private $_langcode;
+ private $_projectIdVersion;
+ private $_potCreationDate;
+ private $_poRevisionDate;
+ private $_languageTeam;
+ private $_mimeVersion;
+ private $_contentType;
+ private $_contentTransferEncoding;
+ private $_pluralForms;
+ private $_authors;
+ private $_po_date;
+ private $_languageName;
+ private $_projectName;
+
+ /**
+ * Creates a PoHeader with default values set.
+ *
+ * @param type $langcode
+ */
+ public function __construct($langcode = NULL) {
+ $this->_langcode = $langcode;
+ $this->setDefaults();
+ }
+
+ static public function mapping() {
+ return array(
+ 'Project-Id-Version' => '_projectIdVersion',
+ // * Report-Msgid-Bugs-To
+ 'POT-Creation-Date' => '_potCreationDate',
+ 'PO-Revision-Date' => '_poRevisionDate',
+ // * Last-Translator
+ 'Language-Team' => '_languageTeam',
+ 'MIME-Version' => '_mimeVersion',
+ // * Language
+ 'Content-Type' => '_contentType',
+ 'Content-Transfer-Encoding' => '_contentTransferEncoding',
+ 'Plural-Forms' => '_pluralForms',
+ );
+ }
+
+ function getPlural() {
+ return $this->_pluralForms;
+ }
+
+ function setLanguageName($languageName) {
+ $this->_languageName = $languageName;
+ }
+
+ function getLanguageName() {
+ return $this->_languageName;
+ }
+
+ function setProjectName($projectName) {
+ $this->_projectName = $projectName;
+ }
+
+ 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;
+ }
+
+ /**
+ * Stores a given PO Header string
+ *
+ * TODO: the header string is cleaned by the parser :(
+ * we need to accept unclean version too
+ *
+ * @param type $header
+ */
+ public function setFromString($header) {
+ $values = $this->parseHeader($header);
+
+ $this->setDefaults($values);
+ }
+
+ /**
+ * TODO: compare with Symfony::setDefaults()
+ *
+ * @param type $values
+ */
+ public function setDefaults($values = array()) {
+ $defaults = array(
+ 'POT-Creation-Date' => date("Y-m-d H:iO"),
+ 'Plural-Forms' => 'nplurals=2; plural=(n > 1);',
+ );
+ foreach ($defaults as $key => $value) {
+ if (empty($values[$key])) {
+ $values[$key] = $value;
+ }
+ }
+ $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 number of plurals and a
+ * formula in PHP for computing the plural form.
+ */
+ 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) {
+ $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.
+ */
+ 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])) { // 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;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Gettext/PoInterface.php b/core/lib/Drupal/Component/Gettext/PoInterface.php
new file mode 100644
index 0000000..8c01ee7
--- /dev/null
+++ b/core/lib/Drupal/Component/Gettext/PoInterface.php
@@ -0,0 +1,31 @@
+ 'home',
+ * 'translation' => 'thuis',
+ * 'plural' => 0,
+ * 'context' => '',
+ * 'comment' => '',
+ * ),
+ * In case we want using just arrays we can make these methods static.
+ *
+ * @author clemens
+ * @see \Drupal\Component\Gettext\Gettext
+ */
+class PoItem {
+
+ /**
+ * The context this translation belongs to.
+ *
+ * The default context should be an empty string
+ * @see PoMemoryWriter.writeItem()
+ * @var string
+ */
+ public $context = '';
+ public $source;
+ public $plural;
+ public $comment;
+ public $translation;
+
+ static public function mapping() {
+ return array(
+ 'msgctxt' => 'context',
+ 'msgid' => 'source',
+ 'msgstr' => 'translation',
+ '#' => 'comment',
+ );
+ }
+
+ public function fromArray(array $values = array()) {
+ foreach ($values as $key => $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);
+ }
+ }
+
+ public function __toString() {
+ return $this->compileTranslation();
+ }
+
+ /**
+ * Compile PO translations strings from a translation object.
+ *
+ * Translation object consists of:
+ * source string (singular) or array of strings (plural)
+ * translation string (singular) or array of strings (plural)
+ * plural TRUE: source and translation are plurals
+ * context source context string
+ */
+ private function compileTranslation() {
+ $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..c64524e
--- /dev/null
+++ b/core/lib/Drupal/Component/Gettext/PoMemoryWriter.php
@@ -0,0 +1,80 @@
+_items = array();
+ }
+
+ public function setState(array $state) {
+ // nothing to do?
+ }
+
+ public function getState() {
+ return array();
+ }
+
+ /**
+ * Stores values into memory.
+ *
+ * The structure is context dependent.
+ * TODO: where is this structure documented?
+ * - array[context][source] = translation
+ *
+ * @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;
+ }
+
+ public function writeItems(PoReaderInterface $reader, $count = -1) {
+ $forever = $count == -1;
+ while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
+ $this->writeItem($item);
+ }
+ }
+
+ public function getHeader() {
+ // TODO: what
+ }
+
+ public function getLangcode() {
+ // TODO: what
+ }
+
+ public function setHeader(PoHeader $header) {
+ // TODO: what
+ }
+
+ public function setLangcode($langcode) {
+ // TODO: what
+ }
+
+ 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..9977767
--- /dev/null
+++ b/core/lib/Drupal/Component/Gettext/PoReaderInterface.php
@@ -0,0 +1,22 @@
+ 'home',
+ 'translation' => 'thuis',
+ 'plural' => 0,
+ 'context' => '',
+ ),
+ array(
+ 'source' => 'delete',
+ 'translation' => 'verwijderen',
+ 'plural' => 0,
+ 'context' => '',
+ ),
+ array(
+ 'source' => array('1 day', '@count days'),
+ 'translation' => array('1 dag', '@count dagen'),
+ 'plural' => 1,
+ 'context' => '',
+ ),
+ );
+ $items = array();
+ foreach ($src as $values) {
+ $item = new PoItem();
+ $item->fromArray($values);
+ $items[] = $item;
+ }
+ $result = array(
+ 'langcode' => $langcode,
+ 'items' => $items,
+ );
+
+ return $result;
+}
+
+function testWriter($uri = '') {
+ if (empty($uri)) {
+ $uri = getPublicUri(__FUNCTION__, $langcode);
+ }
+ zapUri($uri);
+ logLine(__FUNCTION__);
+ $po = gettext_struct();
+ $langcode = $po['langcode'];
+ $items = $po['items'];
+
+ $writer = new PoFileWriter();
+
+ $writer->setLangcode($langcode);
+ $writer->setHeader(new PoHeader($langcode));
+ $writer->setURI($uri);
+ $writer->open();
+ foreach ($items as $item) {
+ $writer->writeItem($item);
+ }
+ $writer->close();
+ return $uri;
+}
+
+function testBatchState() {
+ logLine(__FUNCTION__, '=');
+ $langcode = 'nl';
+ $uri = remoteToPublic($langcode);
+
+ $reader = new PoFileReader();
+ $reader->setLangcode($langcode);
+ $reader->setURI($uri);
+
+ echo "Not yet opened $uri\n";
+ dumpState($reader->getState());
+
+ echo "Opening stream $uri\n";
+ $reader->open();
+ dumpState($reader->getState());
+
+ echo "Reading 1 items\n";
+ $reader->readItem();
+ dumpState($reader->getState());
+
+ echo "Saving state\n";
+ $state = $reader->getState();
+
+ $item = $reader->readItem();
+ echo "Item read after state $item";
+
+ echo "Dropping \$reader\n";
+ $reader = NULL;
+
+ $reader = new PoFileReader();
+ $reader->setLangcode($langcode);
+ $reader->setURI($uri);
+
+ // This is an implicit open!
+ echo "Setting state simulating Batch\n";
+ $reader->setState($state);
+ dumpState($reader->getState());
+ $sameItem = $reader->readItem();
+ echo "These should be the same\n\n$item\n$sameItem\n";
+}
+
+function getReadStream($uri) {
+ logLine(__FUNCTION__, '=');
+ $s = new GettextFileInterface($uri);
+ return $s;
+}
+
+function testPoReader() {
+ logLine(__FUNCTION__, '=');
+
+ $uri = 'public://test.po.txt';
+
+ logLine("Reading : $uri", '=');
+ logLine("File contents first 500 bytes");
+ $contents = file_get_contents($uri);
+ echo substr($contents, 0, 500) . "\n";
+
+ logLine("Using PoFileReader");
+ $reader = new PoFileReader($uri);
+ echo $reader->getHeader();
+
+ $i = 0;
+ while (($item = $reader->readItem()) && $i++ < 4) {
+ printItem($item, $i);
+ }
+}
+
+function testHeader() {
+ logLine(__FUNCTION__, '=');
+ $h = new PoHeader();
+
+ echo "----------------\n";
+ $h->setFromString('');
+ echo "empty header\n";
+ echo $h;
+
+ echo "----------------\n";
+ $h->setFromString('"Project-Id-Version: ' . __FILE__ . '\n"');
+ echo "-- one item -- \n";
+ echo $h;
+}
+
+function testFileToDb() {
+ logLine(__FUNCTION__, '=');
+ $uri = getPublicUri(__FUNCTION__, 'nl');
+ testWriter($uri);
+ logLine("Reading : $uri");
+ logLine("File contents first 500 bytes");
+ $contents = file_get_contents($uri);
+ echo substr($contents, 0, 500) . "\n";
+
+ logLine("POFileReader");
+ $reader = new PoFileReader();
+ $reader->setURI($uri);
+ $reader->open();
+ $header = $reader->getHeader();
+ logLine("Header read");
+ logLine($header);
+
+ $langcode = 'ca';
+ logLine("PoDatabaseWriter");
+ $writer = new PoDatabaseWriter();
+ $writer->setLangcode($langcode);
+ $writer->setHeader($header);
+
+ $i = 0;
+ while (($item = $reader->readItem()) && $i < 4) {
+ logLine($item);
+ $writer->writeItem($item);
+ $i++;
+ }
+ logLine(__FUNCTION__ . " #$i items written.");
+}
+
+function testDbDump() {
+ logLine(__FUNCTION__, '=');
+ $reader = new PoDatabaseReader('en');
+ echo $reader->getHeader() . "\n";
+
+ $i = 0;
+ while (($item = $reader->readItem()) && $i < 4) {
+ printItem($item, $i++);
+ }
+
+ echo "Saving state to simulate a batch\n";
+ $state = $reader->getState();
+
+ echo "Create a new PoDatabaseReader so simulate a batch\n";
+ $reader = new PoDatabaseReader('en');
+
+ // Set the state
+ $reader->setState($state);
+ $i = 0;
+ while (($item = $reader->readItem()) && $i < 4) {
+ printItem($item, $i++);
+ }
+}
+
+function printItem($item, $context = 0) {
+ logLine(__FUNCTION__, '=');
+ if ($item) {
+ logLine("$context : $item->lid");
+ print_r($item);
+ }
+}
+
+function readItem($reader, $context = 0) {
+ $item = $reader->readItem();
+ if ($item) {
+ printItem($item);
+ }
+}
+
+function dumpState($state) {
+ logLine(__FUNCTION__, '=');
+ print_r($state);
+}
+
+function testPOFileReader() {
+ logLine(__FUNCTION__, '=');
+ $uri = 'public://nl-nl.po';
+
+ $reader = new PoFileReader($uri);
+ dumpState($reader->getState());
+ echo $reader->getHeader() . "\n";
+ dumpState($reader->getState());
+ $i = 0;
+ while (($item = readItem($reader)) && $i < 4) {
+ printItem($item, $i++);
+ }
+ dumpState($reader->getState());
+}
+
+function getLanguages($langcode = NULL) {
+ logLine(__FUNCTION__, '=');
+ if (!is_null($langcode)) {
+ return array($langcode);
+ }
+ return array(
+ 'nl',
+ 'ar',
+ 'ca',
+ 'en', // does not exists on d.o (should it be?)
+ 'NOP', // does really not exists on d.o
+ );
+}
+
+function getRemoteUris($langcode = NULL) {
+ logLine(__FUNCTION__, '=');
+ $langcodes = getLanguages($langcode);
+ $uris = array();
+ foreach ($langcodes as $langcode) {
+ $uri = "http://ftp.drupal.org/files/translations/7.x/drupal/drupal-7.11.$langcode.po";
+ $uris[$langcode] = $uri;
+ }
+ return $uris;
+}
+
+function getPublicUri($name, $langcode) {
+ $result = "public://$name-$langcode.po";
+ logLine($result);
+ return $result;
+}
+
+function getRemoteUri($langcode) {
+ $uris = getRemoteUris($langcode);
+ return $uris[$langcode];
+}
+
+function testRemotePOPumper() {
+ logLine(__FUNCTION__, '=');
+ $uris = getRemoteUris();
+ foreach ($uris as $langcode => $uri) {
+ logLine("langcode: $langcode");
+ $reader = new PoFileReader($uri);
+ $writer = new PoDatabaseWriter($langcode);
+ $writer->writeItems($reader, 10);
+ }
+}
+
+function testPOFileWriter() {
+ logLine(__FUNCTION__, '=');
+ $src = "http://ftp.drupal.org/files/translations/7.x/drupal/drupal-7.11.ar.po";
+
+ $reader = new PoFileReader();
+ $reader->setURI($src);
+ $reader->open();
+ $header = $reader->getHeader();
+
+ $dst = 'public://drupal-7.11.ar.po';
+ zapUri($dst);
+ $writer = new PoFileWriter();
+ $writer->setURI($dst);
+ $writer->setHeader($header);
+ $writer->open();
+
+ $i = 0;
+ while (($item = $reader->readItem()) && $i < 4) {
+ printItem($item, $i);
+ $i++;
+ $writer->writeItem($item);
+ dumpState($writer->getState());
+ }
+
+ $writer->close();
+}
+
+function testDbToFile() {
+ logLine(__FUNCTION__, '=');
+ $langcode = 'ca';
+ $reader = new PoDatabaseReader();
+ $reader->setLangcode($langcode);
+
+ $dst = 'public://drupal-7.11.dummy.po';
+ $header = $reader->getHeader();
+
+ $writer = new PoFileWriter();
+ $writer->setURI($dst);
+ $writer->setHeader($header);
+ $writer->open();
+
+ $i = 0;
+ while (($item = $reader->readItem()) && $i < 4) {
+ printItem($item, $i);
+ $i++;
+ $writer->writeItem($item);
+ dumpState($writer->getState());
+ }
+
+ $writer->writeItems($reader, 10);
+
+ $writer->close();
+}
+
+function testBatchSimulation() {
+ logLine(__FUNCTION__, '=');
+
+ // Grab first langcode
+ $uris = getRemoteUris();
+ $langcode = key($uris);
+ $src = current($uris);
+
+ logLine("Opening $langcode : $src");
+ $reader = new PoFileReader();
+ $reader->setURI($src);
+ $reader->open();
+
+ $header = $reader->getHeader();
+ logLine($header);
+
+ $dst = getPublicUri(__FUNCTION__, $langcode);
+
+ zapUri($dst);
+ logLine("Writing $langcode : $dst");
+ $writer = new PoFileWriter();
+ $writer->setURI($dst);
+ $writer->setHeader($header);
+ $writer->open();
+
+ logLine('Written header only', '=');
+ echo file_get_contents($dst);
+
+ processN($writer, $reader, 2);
+
+ dumpFileContents($dst);
+
+ $state = $reader->getState();
+ dumpState($state);
+
+ logLine('Replacing reader', '=');
+ $reader = new PoFileReader($src);
+
+ logLine('setting state back');
+ $reader->setState($state);
+ dumpState($state);
+
+ processN($writer, $reader, 3);
+ $reader->setState($state);
+ dumpState($state);
+
+ dumpFileContents($dst);
+}
+
+function testDBReaderState() {
+ logLine(__FUNCTION__);
+ $langcode = 'nl';
+ $reader = new PoDatabaseReader();
+ $reader->setLangcode($langcode);
+
+ logLine("Init PoDatabaseReader", '=');
+ $state = $reader->getState();
+ dumpState($state);
+
+ $header = $reader->getHeader();
+
+ $uri = getPublicUri(__FUNCTION__, $langcode);
+ zapUri($uri);
+ $writer = new PoFileWriter($uri, $header);
+ $writer->setHeader($header);
+ $writer->setURI($uri);
+ $writer->open();
+
+ processN($writer, $reader, 4);
+
+ logLine("Read some", '=');
+ $state = $reader->getState();
+ dumpState($state);
+
+ $reader = new PoDatabaseReader($langcode);
+ $reader->setState($state);
+ processN($writer, $reader, 4);
+ $state = $reader->getState();
+ dumpState($state);
+
+ logLine("File contents from $uri", '=');
+ echo file_get_contents($uri);
+}
+
+function zapUri($uri) {
+ logLine("Truncate $uri", '=');
+ ftruncate(fopen($uri, 'w'));
+}
+
+function processN($writer, $reader, $count = 10) {
+ if ($count == -1) {
+ logLine("processing items: __ALL__");
+ }
+ else {
+ logLine("processing items: $count");
+ }
+ $writer->writeItems($reader, $count);
+}
+
+function dumpFileContents($uri) {
+ logLine("Written: $uri", '=');
+ echo file_get_contents($uri);
+}
+
+function newPOFileReader($uri, $langcode = NULL) {
+ logLine("Reading from $uri using langcode: '$langcode'");
+ $reader = new PoFileReader();
+ $reader->setURI($uri);
+ $reader->setLangcode($langcode);
+ $reader->open();
+ return $reader;
+}
+
+function remoteToPublic($langcode) {
+ logLine(__FUNCTION__, '=');
+ $uri = getRemoteUri($langcode);
+
+ logLine("Reading from $uri");
+ $reader = newPoFileReader($uri, $langcode);
+ $header = $reader->getHeader();
+
+ $uri = getPublicUri($langcode, $langcode);
+ zapUri($uri);
+ logLine("Writing to $uri");
+ $writer = new PoFileWriter();
+ $writer->setURI($uri);
+ $writer->setHeader($header);
+ $writer->open();
+
+ processN($writer, $reader, -1);
+
+ return $uri;
+}
+
+function publicToDb($langcode) {
+ $uri = getPublicUri($langcode, $langcode);
+
+ logLine("Reading from $uri using langcode: '$langcode'");
+ $reader = new PoFileReader();
+ $reader->setURI($uri);
+ $reader->setLangcode($langcode);
+ $reader->open();
+
+ logLine("Writing to DB");
+ $writer = new PoDatabaseWriter();
+ $writer->setLangcode($langcode);
+ $writer->setHeader($reader->getHeader());
+
+ $locale_plurals = variable_get('locale_translation_plurals', array());
+ print_r(array("Should have $langcode" => $locale_plurals));
+
+ $options = $writer->getOptions();
+ print_r($writer->getOptions());
+ //$options['overwrite_options']['not_customized'] = TRUE;
+ $writer->setOptions($options);
+ print_r($writer->getOptions());
+ print_r($options);
+ processN($writer, $reader, -1);
+
+ print_r($writer->getReport());
+}
+
+function publicToMemory($langcode) {
+ $uri = getPublicUri($langcode, $langcode);
+
+ logLine("Reading from $uri using langcode: '$langcode'");
+ $reader = new PoFileReader();
+ $reader->setURI($uri);
+ $reader->open();
+
+ logLine("Writing to Memory");
+ $writer = new PoMemoryWriter();
+ $writer->setLangcode($langcode);
+ $writer->setHeader($reader->getHeader());
+
+ $locale_plurals = variable_get('locale_translation_plurals', array());
+ print_r(array("Should have $langcode" => $locale_plurals));
+
+ processN($writer, $reader, 5);
+
+ var_dump($writer);
+}
+
+function pumpAround($langcode) {
+ $uri = remoteToPublic($langcode);
+ publicToMemory($langcode);
+ publicToDb($langcode);
+
+ $reader = newPoFileReader($uri, $langcode);
+
+ logLine("Writing to DB");
+ $writer = new PoDatabaseWriter();
+ $writer->setLangcode($langcode);
+
+ processN($writer, $reader);
+
+ logLine("Reading from DB");
+ $reader = new PoDatabaseReader();
+ $reader->setLangcode($langcode);
+ $reader->setOptions(array());
+
+ var_dump($reader->getOptions());
+
+ $header = $reader->getHeader();
+
+ $uri = getPublicUri(__FUNCTION__ . '-db', $langcode);
+ zapUri($uri);
+ logLine("Writing to $uri");
+ $writer = new PoFileWriter();
+ $writer->setURI($uri);
+ $writer->setHeader($header);
+ $writer->open();
+ processN($writer, $reader, -1);
+}
+
+function runAll() {
+ // Make sure all test languages are around
+ testLanguages();
+ testWriter();
+ testDBReaderState();
+ testBatchSimulation();
+ testBatchState();
+ testPoReader();
+ testHeader();
+ testFileToDb();
+ testDbDump();
+ testPOFileReader();
+ testPOFileWriter();
+ testDbToFile();
+ testRemotePOPumper();
+
+ pumpAll();
+
+ testFileToDb();
+
+ testT();
+ testFormatPlural();
+}
+
+function pumpAll() {
+ $uris = getRemoteUris();
+ foreach ($uris as $langcode => $uri) {
+ pumpAround($langcode);
+ }
+}
+
+function testT() {
+ logLine(__FUNCTION__, '=');
+ $uris = getRemoteUris();
+ foreach ($uris as $langcode => $uri) {
+ // TODO create a DBReader with filter like plural / context / etc
+ $sentences = array(
+ 'Home',
+ 'May',
+ 'Website',
+ 'Drupal',
+ 'One',
+ 'Two',
+ );
+
+ foreach ($sentences as $sentence) {
+ $result = t($sentence, array(), array('langcode' => $langcode));
+ echo "$langcode: $sentence => $result\n";
+ }
+ }
+}
+
+function testFormatPlural() {
+ $uris = getRemoteUris();
+ foreach ($uris as $langcode => $uri) {
+ $plurals = array();
+ $plurals[] = array('1 day', '@count days');
+ $plurals[] = array('1 pending update', '@count pending updates');
+ foreach ($plurals as $plural) {
+ for ($i = 0; $i < 200; $i++) {
+ $result = format_plural($i, $plural[0], $plural[1], array('@count' => $i), array('langcode' => $langcode));
+ echo "$langcode: $plural[0] | $plural[1] => $result\n";
+ if ($i > 0) {
+ $i = $i * 3;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Make sure languages are added to the Drupal install.
+ */
+function testLanguages() {
+ $uris = (getRemoteUris());
+ unset($uris['NOP']);
+ $needed = array_keys($uris);
+ $list = language_list();
+ $missing = array_diff($needed, array_keys($list));
+ foreach ($missing as $langcode) {
+ $language = (object) array(
+ 'langcode' => $langcode,
+ 'default' => FALSE,
+ );
+ language_save($language);
+ }
+ if (count($missing)) {
+ echo "The following languages were added to your Drupal install: " . implode(", ", $missing) . "\n";
+ }
+}
+
+//testT();
+//testFormatPlural();
+//$langs = language_list();
+//var_dump($langs);
+//pumpAround('nl');
+runAll();
+//pumpAll();
+
+//testBatchState();
diff --git a/core/modules/locale/lib/Drupal/locale/Gettext.php b/core/modules/locale/lib/Drupal/locale/Gettext.php
new file mode 100644
index 0000000..76d9845
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/Gettext.php
@@ -0,0 +1,86 @@
+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 type $file
+ * @param type $langcode
+ * @param array $files
+ * @return array
+ */
+ static function fileToDatabase($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) {
+ $reader = new PoFileReader();
+ $reader->setLangcode($langcode);
+ $reader->setURI($file->uri);
+
+ try {
+ // When opening header is parsed immediately
+ $reader->open();
+ }
+ catch (Exception $exc) {
+ throw new $exc;
+ }
+
+ $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 $exc) {
+ throw new $exc;
+ }
+
+ 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..e2cfbe2
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php
@@ -0,0 +1,185 @@
+setOptions(array());
+ }
+
+ public function getLangcode() {
+ return $this->_langcode;
+ }
+
+ public function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+ function getOptions() {
+ return $this->_options;
+ }
+
+ function setOptions(array $options) {
+ if (!isset($options['override_options'])) {
+ $options['override_options'] = array();
+ }
+ if (!isset($options['customized'])) {
+ $options['customized'] = LOCALE_NOT_CUSTOMIZED;
+ }
+ $this->_options = array(
+ 'override_options' => $options['override_options'],
+ );
+ $this->_options += $options;
+ }
+
+ function setState(array $state) {
+ $this->_lid = $state['lid'];
+ $this->setOptions($state['options']);
+ $this->buildQuery();
+ }
+
+ function getState() {
+ return array(
+ '__CLASS__' => __CLASS__,
+ 'lid' => $this->_lid,
+ 'options' => $this->_options,
+ );
+ }
+
+ function getHeader() {
+ return new PoHeader($this->getLangcode());
+ }
+
+ public function setHeader(PoHeader $header) {
+ // empty on purpose
+ }
+
+ /**
+ * 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.
+ */
+ 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'));
+
+ // TODO: we need to order by lid
+ // This does not seem to work
+ $query->orderBy('s.lid');
+ $query->condition('s.lid', $this->_lid, '>');
+
+ $this->_result = $query->execute();
+ //echo "Executing: (lid = $this->_lid) : \n" . $this->_result->getQueryString() . "\n";
+ }
+
+ private function getResult() {
+ if (!isset($this->_result)) {
+ $this->buildQuery();
+ }
+ return $this->_result;
+ }
+
+ function readItem() {
+ $result = $this->getResult();
+ $values = $result->fetchAssoc();
+ if ($values) {
+ $poItem = new PoItem();
+ $poItem->fromArray($values);
+ // Manage state
+ $this->_lid = $values['lid'];
+ 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..6386e46
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php
@@ -0,0 +1,302 @@
+ 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,
+ ),
+ );
+ }
+
+ /**
+ * Report array summarizing the number of changes done in the form:
+ * array(additions, updates, deletes, skips, ignored).
+ *
+ * @var array
+ */
+ private $_report;
+
+ /**
+ * @see BatchStateInterface
+ */
+ function __construct() {
+ $this->setState(array());
+ }
+
+ public function getLangcode() {
+ return $this->_langcode;
+ }
+
+ public function setLangcode($langcode) {
+ $this->_langcode = $langcode;
+ }
+
+ public function getReport() {
+ return $this->_report;
+ }
+
+ function setReport($report) {
+ $report += array(
+ 'additions' => 0,
+ 'updates' => 0,
+ 'deletes' => 0,
+ 'skips' => 0,
+ 'ignored' => 0,
+ );
+ $this->_report = $report;
+ }
+
+ function getOptions() {
+ return $this->_options;
+ }
+
+ function setOptions(array $options) {
+ $options['overwrite_options'] += array(
+ 'not_customized' => FALSE,
+ 'customized' => FALSE,
+ );
+ $options += array(
+ 'customized' => LOCALE_NOT_CUSTOMIZED,
+ );
+ $this->_options = $options;
+ }
+
+ /**
+ * Implementation of BatchInterface::setState
+ *
+ * @param array $state
+ */
+ public function setState(array $state) {
+ $state += self::getDefaultState();
+ $this->_report = $state['report'];
+ $this->setLangcode($state['langcode']);
+ $this->setOptions($state['options']);
+ }
+
+ public function getState() {
+ return array(
+ 'class' => __CLASS__,
+ 'report' => $this->getReport(),
+ 'langcode' => $this->getLangcode(),
+ 'options' => $this->getOptions(),
+ );
+ }
+
+ 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();
+ // TODO: this is a sloppy way to create a file name
+ // but parsePluralForms is also weird to me still
+ $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 an item 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 next items from a reader to the database.
+ * If no number of items is specified, all of theme are being written.
+ *
+ * @param PoReaderInterface $reader
+ * @param $count
+ * The number of items that will be wrote.
+ */
+ 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;
+ }
+ }
+
+}
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..d96b40c 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,15 @@ 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) {
+ drupal_set_message(print_r($exc, TRUE));
+ }
}
}
@@ -308,6 +388,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"
+
+
+