diff --git a/core/lib/Drupal/Component/Gettext/BatchStateInterface.php b/core/lib/Drupal/Component/Gettext/BatchStateInterface.php new file mode 100644 index 0000000..df4e59e --- /dev/null +++ b/core/lib/Drupal/Component/Gettext/BatchStateInterface.php @@ -0,0 +1,50 @@ + + * 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' => __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/PoStreamReader.php b/core/lib/Drupal/Component/Gettext/PoStreamReader.php index bd916a7..0e08667 100644 --- a/core/lib/Drupal/Component/Gettext/PoStreamReader.php +++ b/core/lib/Drupal/Component/Gettext/PoStreamReader.php @@ -9,6 +9,7 @@ namespace Drupal\Component\Gettext; use Drupal\Component\Gettext\PoReaderInterface; use Drupal\Component\Gettext\PoStreamInterface; +use Drupal\Component\Gettext\BatchStateInterface; use Drupal\Component\Gettext\PoHeader; /** @@ -17,7 +18,7 @@ use Drupal\Component\Gettext\PoHeader; * The PO file format parsing is implemented according to the documentation at * http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files */ -class PoStreamReader implements PoStreamInterface, PoReaderInterface { +class PoStreamReader implements PoStreamInterface, PoReaderInterface, BatchStateInterface { /** * Source line number of the stream being parsed. @@ -205,6 +206,36 @@ class PoStreamReader implements PoStreamInterface, PoReaderInterface { } /** + * Implements Drupal\Component\Gettext\BatchStateInterface::setState(). + */ + 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']; + } + } + + /** + * Implements Drupal\Component\Gettext\BatchStateInterface::getState(). + */ + public function getState() { + return array( + 'class' => __CLASS__, + 'uri' => $this->_uri, + 'langcode' => $this->_langcode, + 'seekpos' => ftell($this->_fd), + 'lineno' => $this->lineno, + ); + } + + /** * Read the header from the PO stream. * * The header is a special case PoItem, using the empty string as source and diff --git a/core/modules/locale/lib/Drupal/locale/Gettext.php b/core/modules/locale/lib/Drupal/locale/Gettext.php index f3ba3b9..748f817 100644 --- a/core/modules/locale/lib/Drupal/locale/Gettext.php +++ b/core/modules/locale/lib/Drupal/locale/Gettext.php @@ -66,7 +66,7 @@ class Gettext { * * @see Drupal\locale\PoDatabaseWriter */ - static function fileToDatabase($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) { + static function fileToDatabase($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED, $seekpos = NULL, $items = -1) { // Instantiate and initialize the stream reader for this file. $reader = new PoStreamReader(); $reader->setLangcode($langcode); @@ -96,13 +96,25 @@ class Gettext { // Attempt to pipe all items from the file to the database. try { - $writer->writeItems($reader, -1); + if ($seekpos) { + $current_state = $reader->getState(); + // Reset the lineno. + unset($current_state['lineno']); + $current_state['seekpos'] = $seekpos; + $reader->setState($current_state); + } + $writer->writeItems($reader, $items); } catch (Exception $exception) { throw $exception; } // Report back with an array of status information. - return $writer->getReport(); + $report = $writer->getReport(); + + // Add the seek position to the report. + $current_state = $reader->getState(); + $report['seekpos'] = $current_state['seekpos']; + return $report; } } diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 8a9c2fe..1c83a27 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -108,45 +108,8 @@ function locale_translate_import_form_submit($form, &$form_state) { drupal_set_message(t('The language %language has been created.', array('%language' => t($language->name)))); } $customized = $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED; - - // Now import strings into the language - 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); - } + $batch = locale_translate_batch_build(array($file->uri => $file), $form_state['values']['overwrite_options'], $customized, $form_state['values']['langcode'], TRUE); + batch_set($batch); } else { drupal_set_message(t('File to import not found.'), 'error'); @@ -285,10 +248,10 @@ function locale_translate_export_form_submit($form, &$form_state) { /** * Set a batch for newly added language. */ -function locale_translate_add_language_set_batch($langcode) { +function locale_translate_add_language_set_batch($langcode, $overwrite_options = array(), $customized = LOCALE_NOT_CUSTOMIZED) { // See if we have language files to import for the newly added language, // collect and import them. - if ($batch = locale_translate_batch_import_files($langcode, TRUE)) { + if ($batch = locale_translate_batch_import_files($langcode, $overwrite_options, $customized, TRUE)) { batch_set($batch); } } @@ -308,7 +271,7 @@ function locale_translate_add_language_set_batch($langcode) { * l10n_update functionality to feed in translation files alike. * See http://drupal.org/node/1191488. */ -function locale_translate_batch_import_files($langcode = NULL, $finish_feedback = FALSE, $force = FALSE) { +function locale_translate_batch_import_files($langcode = NULL, $overwrite_options = array(), $customized = LOCALE_NOT_CUSTOMIZED, $finish_feedback = FALSE, $force = FALSE) { $files = array(); if (!empty($langcode)) { $langcodes = array($langcode); @@ -335,7 +298,7 @@ function locale_translate_batch_import_files($langcode = NULL, $finish_feedback } } } - return locale_translate_batch_build($files, $finish_feedback); + return locale_translate_batch_build($files, $overwrite_options, $customized, NULL, $finish_feedback); } /** @@ -364,13 +327,13 @@ function locale_translate_get_interface_translation_files($langcode = NULL) { * @return * A batch structure or FALSE if $files was empty. */ -function locale_translate_batch_build($files, $finish_feedback = FALSE) { +function locale_translate_batch_build($files, $overwrite_options = array(), $customized = LOCALE_NOT_CUSTOMIZED, $langcode = NULL, $finish_feedback = FALSE) { $t = get_t(); if (count($files)) { $operations = array(); foreach ($files as $file) { // We call locale_translate_batch_import for every batch operation. - $operations[] = array('locale_translate_batch_import', array($file->uri)); + $operations[] = array('locale_translate_batch_import', array($file->uri, $overwrite_options, $customized, $langcode)); } $batch = array( 'operations' => $operations, @@ -398,21 +361,47 @@ function locale_translate_batch_build($files, $finish_feedback = FALSE) { * @param $context * Contains a list of files imported. */ -function locale_translate_batch_import($filepath, &$context) { +function locale_translate_batch_import($filepath, $overwrite_options, $customized, $langcode = NULL, &$context) { // The filename is either {langcode}.po or {prefix}.{langcode}.po, so // we can extract the language code to use for the import from the end. - if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) { + if ($langcode || preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) { $file = entity_create('file', array('filename' => drupal_basename($filepath), 'uri' => $filepath)); - // We need only the last match - $langcode = array_pop($langcode); + // We need only the last match. + if (is_array($langcode)) { + $langcode = array_pop($langcode); + } try { - $report = GetText::fileToDatabase($file, $langcode, array(), LOCALE_NOT_CUSTOMIZED); - $file->langcode = $langcode; - $file->timestamp = filemtime($file->uri); - locale_translate_update_file_history($file); - $context['results']['files'][$filepath] = $filepath; - $context['results']['stats'][$filepath] = $report; - } catch (Exception $exception) { + if (empty($context['sandbox'])) { + $context['sandbox']['parse_state'] = array( + 'filesize' => filesize($file->uri), + 'chunk_size' => 10, + ); + } + $report = GetText::fileToDatabase($file, $langcode, $overwrite_options, $customized, $context['sandbox']['parse_state']['seekpos'], $context['sandbox']['parse_state']['chunk_size']); + // Not yet finished with reading, mark progress based on size and + // position. + // Maximize the progress bar at 95% before completion because batch API + // could trigger end of operation before file reading is done for floating + // point inaccuracies. See http://drupal.org/node/1089472 + if ($report['seekpos'] < filesize($file->uri)) { + $context['sandbox']['parse_state']['seekpos'] = $report['seekpos']; + $context['finished'] = min(0.95, $report['seekpos'] / filesize($file->uri)); + $context['message'] = t('Importing file: %filename (@percent%)', array('%filename' => $file->filename, '@percent' => (int) ($context['finished'] * 100))); + } + else { + // We are finished here. + $context['finished'] = 1; + $file->langcode = $langcode; + $file->timestamp = filemtime($file->uri); + locale_translate_update_file_history($file); + $context['results']['files'][$filepath] = $filepath; + } + // Add the values from the report to the stats for this file. + foreach ($report as $key => $value) { + $context['results']['stats'][$filepath][$key] += $report[$key]; + } + } + catch (Exception $exception) { $context['results']['files'][$filepath] = $filepath; $context['results']['failed_files'][$filepath] = $filepath; } @@ -448,6 +437,11 @@ function locale_translate_batch_finished($success, $results) { 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); } + menu_router_rebuild(); + + // Clear cache and force refresh of JavaScript translations. + _locale_invalidate_js(); + cache()->deletePrefix('locale:'); } }