diff --git a/core/lib/Drupal/Component/Gettext/PoStreamReader.php b/core/lib/Drupal/Component/Gettext/PoStreamReader.php index 74a9bae..83ad207 100644 --- a/core/lib/Drupal/Component/Gettext/PoStreamReader.php +++ b/core/lib/Drupal/Component/Gettext/PoStreamReader.php @@ -170,7 +170,7 @@ public function open() { $this->readHeader(); } else { - throw new Exception('Cannot open stream without URI set.'); + throw new \Exception('Cannot open stream without URI set.'); } } @@ -185,7 +185,7 @@ public function close() { fclose($this->_fd); } else { - throw new Exception('Cannot close stream that is not open.'); + throw new \Exception('Cannot close stream that is not open.'); } } diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php index 190d8ae..f2660fd 100644 --- a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php @@ -12,7 +12,6 @@ use Drupal\Component\Gettext\PoReaderInterface; use Drupal\locale\TranslationString; use PDO; -use Exception; /** * Gettext PO reader working with the locale module database. @@ -103,7 +102,7 @@ function getHeader() { * Always, because you cannot set the PO header of a reader. */ function setHeader(PoHeader $header) { - throw new Exception('You cannot set the PO header in a reader.'); + throw new \Exception('You cannot set the PO header in a reader.'); } /** diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php index f5b9934..337bd90 100644 --- a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php @@ -13,7 +13,6 @@ use Drupal\Component\Gettext\PoWriterInterface; use Drupal\locale\SourceString; use Drupal\locale\TranslationString; -use Exception; /** * Gettext PO writer working with the locale module database. @@ -161,14 +160,14 @@ function setHeader(PoHeader $header) { // Check for options. $options = $this->getOptions(); if (empty($options)) { - throw new Exception("Options should be set before assigning a PoHeader."); + throw new \Exception("Options should be set before assigning a PoHeader."); } $overwrite_options = $options['overwrite_options']; // Check for langcode. $langcode = $this->_langcode; if (empty($langcode)) { - throw new Exception("Langcode should be set before assigning a PoHeader."); + throw new \Exception("Langcode should be set before assigning a PoHeader."); } if (array_sum($overwrite_options) || empty($locale_plurals[$langcode]['plurals'])) { diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php index 06d292f..5d7ce3a 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php @@ -89,7 +89,7 @@ function mockImportedPoFile($langcode, $timestamp_difference = 0) { // Fill the {locale_file} with a custom timestamp. if ($timestamp_difference != 0) { - locale_translate_update_file_history($file); + locale_translate_updation_file_history($file); } $count = db_query('SELECT COUNT(*) FROM {locale_file} WHERE langcode = :langcode', array(':langcode' => $langcode))->fetchField(); diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc index 215cb75..c4b6acd 100644 --- a/core/modules/locale/locale.batch.inc +++ b/core/modules/locale/locale.batch.inc @@ -6,42 +6,6 @@ */ /** - * Build a batch to get the status of remote and local translation files. - * - * The batch process fetches the state of both remote and (if configured) local - * translation files. The data of the most recent translation is stored per - * per project and per language. This data is stored in a state variable - * 'locale_translation_status'. The timestamp it was last updated is stored - * in the state variable 'locale_translation_status_last_update'. - * - * @param array $sources - * Array of translation source objects for which to check the state of - * translation source files. - */ -function locale_translation_batch_status_build($sources) { - $operations = array(); - - // Set the batch processes for remote sources. - foreach ($sources as $source) { - $operations[] = array('locale_translation_batch_status_fetch_remote', array($source)); - } - - // Check for local sources, compare the results of local and remote and store - // the most recent. - $operations[] = array('locale_translation_batch_status_fetch_local', array($sources)); - $operations[] = array('locale_translation_batch_status_compare', array()); - - $batch = array( - 'operations' => $operations, - 'title' => t('Checking available translations'), - 'finished' => 'locale_translation_batch_status_finished', - 'error_message' => t('Error checking available interface translation updates.'), - 'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc', - ); - return $batch; -} - -/** * Batch operation callback: Check the availability of a remote po file. * * Checks the presence and creation time of one po file per batch process. The @@ -62,12 +26,12 @@ function locale_translation_batch_status_fetch_remote($source, &$context) { // data with the remote status. if (isset($source->files['remote'])) { $remote_file = $source->files['remote']; - $result = locale_translation_http_check($remote_file->url); + $result = locale_translation_http_check($remote_file->uri); // Update the file object with the result data. In case of a redirect we - // store the resulting url. + // store the resulting uri. if ($result && !empty($result->updated)) { - $remote_file->url = isset($result->redirect_url) ? $result->redirect_url : $remote_file->url; + $remote_file->uri = isset($result->redirect_uri) ? $result->redirect_uri : $remote_file->uri; $remote_file->timestamp = $result->updated; $source->files['remote'] = $remote_file; } @@ -128,6 +92,7 @@ function locale_translation_batch_status_fetch_local($sources, &$context) { */ function locale_translation_batch_status_compare(&$context) { module_load_include('compare.inc', 'locale'); + $history = locale_translation_get_file_history(); $results = array(); foreach ($context['results']['sources'] as $project => $langcodes) { @@ -141,8 +106,32 @@ function locale_translation_batch_status_compare(&$context) { if (isset($file->timestamp)) { $source->type = $file->type; $source->timestamp = $file->timestamp; - $results[$project][$langcode] = $source; } + + // Compare the available translation with the current translations status. + // If the project/language was translated before and it is more recent + // than the most recent translation, the translation is up to date. Which + // is marked in the source object with type "current". + if (isset($history[$source->project][$source->language])) { + $current = $history[$source->project][$source->language]; + // Add the current translation to the source object to save it in + // the status cache. + $source->files['current'] = $current; + + if (isset($source->type)) { + $available = $source->files[$source->type]; + $result = _locale_translation_source_compare($current, $available) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $available : $current; + $source->type = $result->type; + $source->timestamp = $result->timestamp; + } + else { + $source->type = $current->type; + $source->timestamp = $current->timestamp; + } + } + + + $results[$project][$langcode] = $source; } } @@ -160,9 +149,9 @@ function locale_translation_batch_status_compare(&$context) { function locale_translation_batch_status_finished($success, $results) { $t = get_t(); if($success) { - if ($results) { + if ($results['sources']) { drupal_set_message(format_plural( - count($results), + count($results['sources']), 'Checked available interface translation updates for one project.', 'Checked available interface translation updates for @count projects.' )); @@ -174,46 +163,6 @@ function locale_translation_batch_status_finished($success, $results) { } /** - * Create a batch to just download files. - * - * @param $updates - * Translations sources to be downloaded. - * Note: All update sources must have a 'url'. - * @return array - * A batch definition for this download. - */ -// @todo convert this function. Is this function required? -function l10n_update_batch_download($updates) { - foreach ($updates as $update) { - $id = $update->filename; - $operations[] = array('_l10n_update_batch_download', array($id, $update)); - } - return _l10n_update_create_batch($operations); -} - -/** - * Create a batch to just import files. - * - * All update sources must have a 'uri'. - * - * @param $updates - * Translations sources to be imported. - * Note: All update sources must have a 'url'. - * @param $import_mode - * Import mode. How to treat existing and modified translations. - * @return array - * A batch definition for this import. - */ -// @todo convert this function. Is this function required? -function l10n_update_batch_import($updates, $import_mode) { - foreach ($updates as $update) { - $id = $update->filename; - $operations[] = array('_l10n_update_batch_import', array($id, $update, $import_mode)); - } - return _l10n_update_create_batch($operations); -} - -/** * Batch operation: Download a remote translation file. * * @param $translation source object @@ -230,9 +179,9 @@ function locale_translation_batch_fetch_download($source, &$context) { } else { // @todo Error handling when download fails. - $source->files['remote']->error = 'TODO download error'; + watchdog('locale', 'Unable to download translation file @uri.', array('@uri' => $source->files['remote']->uri), WATCHDOG_ERROR); + $context['results']['failed_files'][] = $source->files['remote']; } -//dpm($source); $context['results']['sources'][$source->name][$source->language] = $source; } @@ -255,26 +204,30 @@ function locale_translation_batch_fetch_download($source, &$context) { */ function locale_translation_batch_fetch_import($source, $options, &$context) { $t = get_t(); - // Import a downloaded file. + // If we are working on a remote file we will import the downloaded file and + // use the result data from the if ($source->type == 'remote') { if (isset($context['results']['sources'][$source->project][$source->language]->files['download'])) { - $file = $context['results']['sources'][$source->project][$source->language]->files['download']; + $import_type = 'download'; + $source_type = 'remote'; + $source_result = $context['results']['sources'][$source->project][$source->language]; } } - else { - $file = $source->files['local']; + if ($source->type == 'local') { + $import_type = 'local'; + $source_type = 'local'; + $source_result = $source; } -//dpm($file); + // @todo Fault handling using try/catch. For other batch functions too? - if (isset($file)) { + if (isset($import_type)) { + $file = $source_result->files[$import_type]; module_load_include('bulk.inc', 'locale'); $options += array( 'customized' => LOCALE_NOT_CUSTOMIZED, 'message' => 'Importing: ' . $file->filename, ); - // @todo Not elegant, is there a better way for both local and remote imports? - $file->langcode = $source->language; locale_translate_batch_import($file, $options, $context); // Import is finished. @@ -283,53 +236,46 @@ function locale_translation_batch_fetch_import($source, $options, &$context) { if (isset($context['results']['files'][$file->uri])) { $context['message'] = $t('Imported: %name.', array('%name' => $file->filename)); - // @todo store data of imported file preparing the data to be saved in {locale_file}. - $source->files['import'] = $file; - - // @todo process $context['stats'] + // Store the data of imported source. This data can later be saved in {locale_file}. + $source_result->files['import'] = $source_result->files[$source_type]; // Downloaded files are stored in the temporary files directory. On // succesfull import, the files are moved to the local destination // directory if files should be kept locally. Otherwise the temporary // files are deleted. - if ($file->type == 'download') { - if (variable_get('locale_translate_file_directory', conf_path() . '/files/translations') && isset($source->files['local'])) { - // Move the temporary file to the local translations directory and - // update the local file data. - $destination = $file->directory . $file->filename; - // @todo Does this work in the file can not be moved? - if (file_unmanaged_move($file->uri, $destination, FILE_EXISTS_REPLACE)) { - $timestamp = filemtime($destination); - $source->files['local']->timestamp = $timestamp; - $source->files['import']->timestamp = $timestamp; - $source->files['import']->last_checked = $timestamp; - $source->files['import']->uri = $source->files['local']->directory . $source->files['local']->filename; - } - else { - // The temporary file could not be moved. Record the error and - // delete the temporary file. - // @todo Throw error? Watchdog or message? - unset($source->files['local']->timestamp); - $source->files['local']->error = 'TODO failure writing the file'; - file_unmanaged_delete($file->uri); - } - } - else { - // The temporary file is now imported and will not be used further. - // It can be deleted. - file_unmanaged_delete($file->uri); + if ($import_type == 'download' && variable_get('locale_translate_file_directory', conf_path() . '/files/translations') && isset($source_result->files['local'])) { + // Move the temporary file to the local translations directory. + // @todo What to do if the file can not be moved? If it throws an error (watchdog), should we set a batch error? + if (file_unmanaged_move($file->uri, $source_result->files['local']->uri, FILE_EXISTS_REPLACE)) { + // The downloaded file is now moved to the local file location. + // From this point forward we can treat it as if we imported a + // local file. + $import_type = 'local'; } } + if ($import_type == 'download') { + // The temporary file is now imported and will not be used further. + // Store the timestamp and delete the file. + $timestamp = filemtime($source_result->files[$import_type]->uri); + $source_result->files['import']->timestamp = $timestamp; + $source_result->files['import']->last_checked = $timestamp; + file_unmanaged_delete($file->uri); + } + if ($import_type == 'local') { + // Import type is 'local'. + $timestamp = filemtime($source_result->files[$import_type]->uri); + $source_result->files['local']->timestamp = $timestamp; + $source_result->files['import']->timestamp = $timestamp; + $source_result->files['import']->last_checked = $timestamp; + + } } else { // File import failed. We can delete the temporary file. file_unmanaged_delete($file->uri); } } - } - - if (isset($context['finished']) && $context['finished'] == 1) { - $context['results']['sources'][$source->name][$source->language] = $source; + $context['results']['sources'][$source->project][$source->language] = $source_result; } } @@ -345,37 +291,25 @@ function locale_translation_batch_fetch_import($source, $options, &$context) { */ function locale_translation_batch_fetch_update_status(&$context) { $results = array(); -//dpm($context['results']); foreach ($context['results']['sources'] as $project => $langcodes) { foreach ($langcodes as $langcode => $source) { - // Store the state of the translation. The data of the imported file was kept - // in $context['results']['sources'] will now be stored in the database. + // Store the state of the translation in {locale_file} table. During the + // batch execution the data of imported file is kept in + // $context['results']['sources'] will now be stored in the database. if (isset($source->files['import'])) { $file = $source->files['import']; - /* {locale_file} contains: - - project - - langcode - - filename - - version - - uri - - last_checked - - timestamp - */ - $file->project = $source->project; - $file->langcode = $source->language; - $file->version = $source->version; - locale_translate_update_file_history($file); + locale_translation_update_file_history($file); + // @todo Unset. Leave it disabled for debugging. //unset($source->files['import']); } + // @todo Unset. Leave it disabled for debugging. //unset($source->files['download']); - //$results[$project][$langcode] = $source; - - // Compare sources and translation history again. Translations have been - // imported which will result in a different translation status. - // @todo Compare and save the translation state. - //locale_translation_status_save($data); + // The source data is now up to date. Data of local and/or remote source + // file is up to date including an updated time stamp. In a next batch + // operation this can be use to update the translation status. + $context['results']['sources'][$project][$langcode] = $source; } } } @@ -389,99 +323,24 @@ function locale_translation_batch_fetch_update_status(&$context) { * Batch results. */ function locale_translation_batch_fetch_finished($success, $results) { - $totals = array(); // Sum of added, updated and deleted translations. - $total_skip = 0; // Sum of skipped translations - $messages = array(); // User feedback messages. - $project_fail = $project_success = array(); // Project names of succesfull and failed imports. - $t = get_t(); -return; - if ($success) { - // Summarize results of added, updated, deleted and skiped translations. - // Added, updated and deleted are summarized per language to be displayed accordingly. - foreach ($results as $result) { - if (isset($result['fail'])) { - // Collect project names of the failed imports. - $project_fail[$result['file']->name] = $result['file']->name; - } - else { - $language = $result['language']; - - // Initialize variables to prevent PHP Notices. - if (!isset($totals[$language])) { - $totals[$language] = array(); - $totals[$language]['add'] = $totals[$language]['update'] = $totals[$language]['delete'] = 0; - } - - // Summarize added, updated, deleted and skiped translations. - $totals[$language]['add'] += $result['add']; - $totals[$language]['update'] += $result['update']; - $totals[$language]['delete'] += $result['delete']; - $total_skip += $result['skip']; - - // Collect project names of the succesfull imports. - $project_success[$result['file']->name] = $result['file']->name; - } - } - - // Messages of succesfull translation update results. - if ($project_success) { - $messages[] = format_plural(count($project_success), 'One project updated: @projects.', '@count projects updated: @projects.', array('@projects' => implode(', ', $project_success))); - $languages = locale_translatable_language_list(); - foreach ($totals as $langcode => $total) { - $messages[] = $t('%language translation strings added: !add, updated: !update, deleted: !delete.', array( - '%language' => $languages[$langcode]->name, - '!add' => $total['add'], - '!update' => $total['update'], - '!delete' => $total['delete'], - )); - } - drupal_set_message(implode("
\n", $messages)); - - // Warning for disallowed HTML. - if ($total_skip) { - drupal_set_message( - format_plural( - $total_skip, - 'One translation string was skipped because it contains disallowed HTML. See !log_messages for details.', - '@count translation strings were skipped because they contain disallowed HTML. See !log_messages for details.', - array('!log_messages' => l(t('Recent log messages'), 'admin/reports/dblog'))), - 'warning'); - } - - // Clear cache and force refresh of JavaScript translations. - _locale_invalidate_js(); - cache()->invalidateTags(array('locale' => TRUE)); - } - - // Error for failed imports. - if ($project_fail) { - drupal_set_message( - format_plural( - count($project_fail), - 'Translations of one project were not imported: @projects.', - 'Translations of @count projects were not imported: @projects', - array('@projects' => implode(', ', $project_fail))), - 'error'); - } - } - else { - drupal_set_message($t('Error importing translations.'), 'error'); - } + // @todo move locale_translate_batch_finished() into locale.batch.inc. + module_load_include('bulk.inc', 'locale'); + return locale_translate_batch_finished($success, $results); } /** * Check if remote file exists and when it was last updated. * - * @param string $url - * URL of remote file. + * @param string $uri + * URI of remote file. * @param array $headers * HTTP request headers. * @return stdClass * Result object containing the HTTP request headers, response code, headers, * data, redirect status and updated timestamp. */ -function locale_translation_http_check($url, $headers = array()) { - $result = drupal_http_request($url, array('headers' => $headers, 'method' => 'HEAD')); +function locale_translation_http_check($uri, $headers = array()) { + $result = drupal_http_request($uri, array('headers' => $headers, 'method' => 'HEAD')); if ($result && $result->code == '200') { $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0; } diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index a8242cf..8b27cde 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -405,13 +405,12 @@ function locale_translate_batch_build($files, $options) { if (count($files)) { $operations = array(); foreach ($files as $file) { - // @todo: make the regex work with missing version. - // @todo: move the language part to the form submit function. + // @todo: Check if regex works with missing project and version. + // @todo: move the language part to the form submit function? // The filename is either {langcode}.po or {prefix}.{langcode}.po. // Try to get the project name, project version and language code file name. // The filename is either {langcode}.po or {project}-{version}.{langcode}.po. // We use the language from either the $options or the file name. - // @todo Perhaps more strict on the langcode syntax? preg_match('!([a-z_]+)\-([0-9a-z\.\-\+]+)\.([^\./]+)\.po$!', $file->filename, $matches); if (is_array($matches)) { $file->project = isset($matches[1]) ? $matches[1] : ''; @@ -443,6 +442,7 @@ function locale_translate_batch_build($files, $options) { /** * Perform interface translation import as a batch step. * + * @todo Rework description. * The given filepath is matched against ending with '{langcode}.po'. When * matched the filepath is added to batch context. * @@ -486,6 +486,7 @@ function locale_translate_batch_import($file, $options, &$context) { // If not yet finished with reading, mark progress based on size and // position. if ($report['seek'] < filesize($file->uri)) { + $context['sandbox']['parse_state']['seek'] = $report['seek']; // Maximize the progress bar at 95% before completion, the batch API // could trigger the end of the operation before file reading is done, @@ -521,6 +522,7 @@ function locale_translate_batch_import($file, $options, &$context) { elseif (is_array($value)) { $context['results']['stats'][$file->uri] += array($key => array()); $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value); + } if (is_numeric($report[$key])) { if (!isset($context['results']['stats'][$file->uri][$key])) { $context['results']['stats'][$file->uri][$key] = 0; @@ -531,7 +533,8 @@ function locale_translate_batch_import($file, $options, &$context) { } catch (Exception $exception) { // Import failed. Store the data of the failing file. - $context['results']['failed_files'][$file->uri] = $file; + $context['results']['failed_files'][] = $file; + watchdog('locale', 'Unable to import translations file: @file', array('@file' => $file->uri)); } } } @@ -545,7 +548,7 @@ function locale_translate_batch_import($file, $options, &$context) { function locale_translate_batch_import_save($context) { foreach ($context['results']['files'] as $file) { $file->last_checked = $file->timestamp; - locale_translate_update_file_history($file); + locale_translation_update_file_history($file); } } @@ -556,42 +559,48 @@ function locale_translate_batch_finished($success, $results) { if ($success) { $additions = $updates = $deletes = $skips = 0; $strings = $langcodes = array(); - if (isset($results['files'])) { - drupal_set_message(format_plural(count($results['files']), 'One translation file imported.', '@count translation files imported.')); - } if (isset($results['failed_files'])) { - drupal_set_message(format_plural(count($results['failed_files']), 'One translation file could not be imported.', '@count translation files could not be imported.'), 'error'); - } - $skipped_files = array(); - // If there are no results and/or no stats (eg. coping with an empty .po - // file), simply do nothing. - if ($results && isset($results['stats'])) { - 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; + if (module_exists('dblog')) { + $message = format_plural(count($results['failed_files']), 'One translation file could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.', array('@url' => url('admin/reports/dblog'))); } - $strings = array_merge($strings, $report['strings']); - } - // Get list of unique string identifiers and language codes updated. - $strings = array_unique($strings); - $langcodes = array_unique(array_values($results['languages'])); + else { + $message = format_plural(count($results['failed_files']), 'One translation files could not be imported. See the log for details.', '@count translation files could not be imported. See the log for details.'); + } + drupal_set_message($message, 'error'); } - 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'))); + if (isset($results['files'])) { + drupal_set_message(format_plural(count($results['files']), 'One translation file imported.', '@count translation files imported.')); + + $skipped_files = array(); + // If there are no results and/or no stats (eg. coping with an empty .po + // file), simply do nothing. + if ($results && isset($results['stats'])) { + 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; + } + $strings = array_merge($strings, $report['strings']); + } + // Get list of unique string identifiers and language codes updated. + $strings = array_unique($strings); + $langcodes = array_unique(array_values($results['languages'])); } - 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(t('The translations were successfully imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes))); + watchdog('locale', 'The translations were successfully imported. %number translations added, %update updated and %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)); + if ($skips) { + if (module_exists('dblog')) { + $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 { + $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($message, 'error'); + watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING); } - 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); - } if ($strings) { // Clear cache and force refresh of JavaScript translations. diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index e69ce1f..13604c6 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -261,7 +261,7 @@ function _locale_translation_prepare_project_list($data, $type) { * * @return array * Array of server parameters: - * - "server_pattern": URL containing po file pattern. + * - "server_pattern": URI containing po file pattern. */ function locale_translation_default_translation_server() { $config = config('locale.settings'); @@ -345,12 +345,47 @@ function locale_translation_check_projects_batch($projects, $langcodes = NULL) { } // Build and set the batch process. - module_load_include('batch.inc', 'locale'); $batch = locale_translation_batch_status_build($sources); batch_set($batch); } /** + * Build a batch to get the status of remote and local translation files. + * + * The batch process fetches the state of both remote and (if configured) local + * translation files. The data of the most recent translation is stored per + * per project and per language. This data is stored in a state variable + * 'locale_translation_status'. The timestamp it was last updated is stored + * in the state variable 'locale_translation_status_last_update'. + * + * @param array $sources + * Array of translation source objects for which to check the state of + * translation source files. + */ +function locale_translation_batch_status_build($sources) { + $operations = array(); + + // Set the batch processes for remote sources. + foreach ($sources as $source) { + $operations[] = array('locale_translation_batch_status_fetch_remote', array($source)); + } + + // Check for local sources, compare the results of local and remote and store + // the most recent. + $operations[] = array('locale_translation_batch_status_fetch_local', array($sources)); + $operations[] = array('locale_translation_batch_status_compare', array()); + + $batch = array( + 'operations' => $operations, + 'title' => t('Checking available translations'), + 'finished' => 'locale_translation_batch_status_finished', + 'error_message' => t('Error checking available interface translation updates.'), + 'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc', + ); + return $batch; +} + +/** * Check and store the status and timestamp of local po files. * * Only po files in the local file system are checked. Any remote translation @@ -381,8 +416,8 @@ function locale_translation_check_projects_local($projects, $langcodes = NULL) { if (locale_translation_source_check_file($source)) { $source->type = 'local'; $source->timestamp = $source->files['local']->timestamp; - $results[$name][$langcode] = $source; } + $results[$name][$langcode] = $source; } } @@ -427,7 +462,7 @@ function locale_translation_source_check_file(&$source) { $directory = str_replace($scheme . '://', drupal_realpath($scheme . '://'), $directory); } - if ($files = file_scan_directory($directory, $filename, array('key' => 'name'))) { + if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) { $file = current($files); $source->files['local']->uri = $file->uri; $source->files['local']->timestamp = filemtime($file->uri); @@ -461,15 +496,19 @@ function locale_translation_source_check_file(&$source) { * - "type": Most recent file type 'remote' or 'local'. Corresponding with * a key of the "files" array. * - "timestamp": Timestamp of the most recent translation file. - * The "files" array contains file objects with the following properties: - * - "uri": Local file path. - * - "url": Remote file URL for downloads. + * The "files" array can hold file objects of type: 'local', 'remote', + * 'download', 'import' and 'current'. Each contains following properties: + * - "type": The object type ('local', 'remote', etc. see above). + * - "project": Project name. + * - "langcode": Language code. + * - "version": Project version. + * - "uri": Local or remote file path. * - "directory": Directory of the local po file. * - "filename": File name. * - "timestamp": Timestamp of the file. * - "keep": TRUE to keep the downloaded file. */ -// @todo Move this file? +// @todo Move this function? function locale_translation_source_build($project, $langcode, $filename = NULL) { // Create a source object with data of the project object. $source = clone $project; @@ -486,9 +525,12 @@ function locale_translation_source_build($project, $langcode, $filename = NULL) $files = array(); if (_locale_translation_file_is_remote($source->server_pattern)) { $files['remote'] = (object) array( + 'project' => $project->name, + 'langcode' => $langcode, + 'version' => $project->version, 'type' => 'remote', 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)), - 'url' => locale_translation_build_server_pattern($source, $source->server_pattern), + 'uri' => locale_translation_build_server_pattern($source, $source->server_pattern), ); if (variable_get('locale_translate_file_directory', conf_path() . '/files/translations')) { $files['local'] = (object) array( @@ -496,17 +538,22 @@ function locale_translation_source_build($project, $langcode, $filename = NULL) 'langcode' => $langcode, 'version' => $project->version, 'type' => 'local', - 'directory' => 'translations://', 'filename' => locale_translation_build_server_pattern($source, $filename), + 'directory' => 'translations://', ); + $files['local']->uri = $files['local']->directory . $files['local']->filename; } } else { $files['local'] = (object) array( + 'project' => $project->name, + 'langcode' => $langcode, + 'version' => $project->version, 'type' => 'local', - 'directory' => locale_translation_build_server_pattern($source, drupal_dirname($source->server_pattern)), 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)), + 'directory' => locale_translation_build_server_pattern($source, drupal_dirname($source->server_pattern)), ); + $files['local']->uri = $files['local']->directory . '/' . $files['local']->filename; } $source->files = $files; @@ -516,14 +563,14 @@ function locale_translation_source_build($project, $langcode, $filename = NULL) /** * Determine if a file is a remote file. * - * @param string $url - * The URL or URL pattern of the file. + * @param string $uri + * The URI or URI pattern of the file. * * @return boolean - * TRUE if the $url is a remote file. + * TRUE if the $uri is a remote file. */ -function _locale_translation_file_is_remote($url) { - $scheme = file_uri_scheme($url); +function _locale_translation_file_is_remote($uri) { + $scheme = file_uri_scheme($uri); if ($scheme) { return !drupal_realpath($scheme . '://'); } diff --git a/core/modules/locale/locale.fetch.inc b/core/modules/locale/locale.fetch.inc index d053bce..e628faa 100755 --- a/core/modules/locale/locale.fetch.inc +++ b/core/modules/locale/locale.fetch.inc @@ -1,81 +1,20 @@ $file->project, ':language' => $file->language))->fetchField()) { - $update = array('project', 'language'); - } - else { - $update = array(); - } - return drupal_write_record('locale_file', $file, $update); -} - -/** - * Delete the history of downloaded translations. - * - * @param string $langcode - * Language code of the file history to be deleted. - */ -function locale_translation_file_history_delete($langcode) { - db_delete('l10n_update_file') - ->condition('language', $langcode) - ->execute(); -} - -/** - * Flag the file history as up to date. - * - * Compare history data in the {l10n_update_file} table with translations - * available at translations server(s). Update the 'last_checked' timestamp of - * the files which are up to date. - * - * @param $available - * Available translations as retreived from remote server. - */ -function l10n_update_flag_history($available) { - if ($history = l10n_update_get_history()) { - foreach($history as $name => $project) { - foreach ($project as $langcode => $current) { - if (isset($available[$name][$langcode])) { - $update = $available[$name][$langcode]; - // When the available update is equal to the current translation the current - // is marked checked in the {l10n_update_file} table. - if (_l10n_update_source_compare($current, $update) == 0 && $current->version == $update->version) { - db_update('l10n_update_file') - ->fields(array( - 'last_checked' => REQUEST_TIME, - )) - ->condition('project', $current->project) - ->condition('language', $current->language) - ->execute(); - } - } - } - } - } -} - -/** - * @todo + * @param array $sources + * Array of project translation states. */ function locale_translation_batch_fetch_build($sources) { $t = get_t(); $batch = array( - 'operations' => locale_translation_fetch_operations($sources), + 'operations' => _locale_translation_fetch_operations($sources), 'title' => $t('Updating translation.'), 'init_message' => $t('Downloading and importing files.'), 'error_message' => $t('Error importing interface translations'), @@ -86,9 +25,12 @@ function locale_translation_batch_fetch_build($sources) { } /** - * @todo + * Helper function to construct the batch operations to fetch translations. + * + * @param array $sources + * Array of project translation states. */ -function locale_translation_fetch_operations($sources) { +function _locale_translation_fetch_operations($sources) { $operations = array(); $config = config('locale.settings'); // @todo Make follow up issue for different UI options. Back to radio's, not current checkboxes. @@ -100,115 +42,55 @@ function locale_translation_fetch_operations($sources) { ); foreach ($sources as $source) { - if ($source->type == 'remote') { - // Download + import - $operations[] = array('locale_translation_batch_fetch_download', array($source)); - $operations[] = array('locale_translation_batch_fetch_import', array($source, $options)); - } - elseif ($source->type == 'local') { - // Import local files. - $operations[] = array('locale_translation_batch_fetch_import', array($source, $options)); + if (isset($source->type)) { + if ($source->type == 'remote') { + // Download + import + $operations[] = array('locale_translation_batch_fetch_download', array($source)); + $operations[] = array('locale_translation_batch_fetch_import', array($source, $options)); + } + elseif ($source->type == 'local') { + // Import local files. + $operations[] = array('locale_translation_batch_fetch_import', array($source, $options)); + } } } - // Update and save the source status. New translation files have been - // downloaded, so other sources will be newer. We update the status now. - $operations[] = array('locale_translation_batch_status_compare', array()); - // Update translation status - $operations[] = array('locale_translation_batch_fetch_update_status', array()); + if (count($operations)) { + // Update and save the translation status. + $operations[] = array('locale_translation_batch_fetch_update_status', array()); + // Update and save the source status. New translation files have been + // downloaded, so other sources will be newer. We update the status now. + $operations[] = array('locale_translation_batch_status_compare', array()); + } return $operations; } /** - * Download source file from remote server. + * Download source file from a remote server. * - * If succesful this function returns the downloaded file in two ways: - * - As a temporary $file object - * - As a file path on the $source->uri property. + * The downloaded file is stored in the temporary files directory. * - * @param $source - * Source object with all parameters - * - 'url': url to download. - * - 'uri': alternate destination. If not present a temporary file - * will be used and the path returned here. + * @param object $source_file + * Source file object with at least: + * - "uri": uri to download the file from. + * - "project": Project name. + * - "langcode": Translation language. + * - "version": Project version. + * - "filename": File name. * @return object - * $file object if download successful. + * File object if download successful. + * FALSE on failure. */ -// @todo Can we skip this (wrapper) function? function locale_translation_download_source($source_file) { - if ($uri = locale_translation_download_file($source_file->url)) { + if ($uri = system_retrieve_file($source_file->uri, 'temporary://')) { $file = new stdClass(); + $file->project = $source_file->project; + $file->langcode = $source_file->langcode; + $file->version = $source_file->version; $file->type = 'download'; $file->uri = $uri; $file->filename = $source_file->filename; return $file; } -} - -/** - * Get remote file and download it to a temporary path. - * - * @param $download_url - * URL of remote file. - * @param $destination - * URL of local destination file. By default the download will be stored - * in a temporary file. - */ -// @todo Use system_retrieve_file() here? -function locale_translation_download_file($download_url, $destination = NULL) { - $t = get_t(); - $variables['%download_link'] = $download_url; - - // Create temporary file or use specified file destination. - // Temporary files get a 'locale-translation-' file name prefix. - // @todo Do we still need the ability to override $destination? - $file = $destination ? $destination : drupal_tempnam('temporary://', 'locale-translation-'); - - if ($file) { - $variables['%tmpfile'] = $file; - // We download and store the file. - // @todo remove the timeout once we use the batch API to download the files. - if (($contents = drupal_http_request($download_url, array('timeout' => 90))) && $contents->code == 200 && $file_result = file_put_contents($file, $contents->data)) { - watchdog('locale', 'Successfully downloaded %download_link to %tmpfile', $variables); - return $file; - } - else { - if (isset($contents->error)) { - watchdog('locale', 'An error occured during the download operation: %error.', array('%error' => $contents->error), WATCHDOG_ERROR); - } - elseif (isset($contents->code) && $contents->code != 200) { - watchdog('locale', 'An error occured during the download operation: HTTP status code %code.', array('%code' => $contents->code), WATCHDOG_ERROR); - } - if (isset($file_result)) { - // file_put_contents() was called but returned FALSE. - watchdog('locale', 'Unable to save %download_link file to %tmpfile.', $variables, WATCHDOG_ERROR); - } - } - - } - else { - $variables['%tmpdir'] = file_directory_temp(); - watchdog('l10n_update', 'Error creating temporary file for download in %tmpdir. Remote file is %download_link.', $variables, WATCHDOG_ERROR); - } -} - -/** - * Download and import remote translation file. - * - * @param $download_url - * Download URL. - * @param $locale - * Language code. - * @param $mode - * Download mode. How to treat exising and modified translations. - * - * @return boolean - * TRUE on success. - */ -// @todo: Do we need this function? -function l10n_update_download_import($download_url, $locale, $mode = LOCALE_IMPORT_OVERWRITE) { - if ($file = l10n_update_download_file($download_url)) { - $result = l10n_update_import_file($file, $locale, $mode); - return $result; - } + return FALSE; } diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index f77aab4..cfc4931 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -103,6 +103,11 @@ const LOCALE_TRANSLATION_DEFAULT_FILENAME = '%project-%version.%language.po'; /** + * The number of seconds that the translations status entry should be considered. + */ +const LOCALE_TRANSLATION_STATUS_TTL = 600; + +/** * Implements hook_help(). */ function locale_help($path, $arg) { @@ -179,7 +184,8 @@ function locale_menu() { $items['admin/reports/translations'] = array( 'title' => 'Available translation updates', 'description' => 'Get a status report about available interface translations for your installed modules and themes.', - 'page callback' => 'locale_translation_status', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_translation_status_form'), 'access arguments' => array('translate interface'), 'file' => 'locale.pages.inc', ); @@ -745,6 +751,27 @@ function locale_preprocess_node(&$variables) { } /** + * Get current translation status from the {locale_file} table. + * + * @return array + * Array of translation file objects. + */ +// @todo Is this the right place for this function? +function locale_translation_get_file_history() { + $history = &drupal_static(__FUNCTION__, array()); + + if (empty($history)) { + // Get file history from the database. + $result = db_query('SELECT project, langcode, filename, version, uri, timestamp, last_checked FROM {locale_file}'); + foreach ($result as $file) { + $file->type = 'current'; + $history[$file->project][$file->langcode] = $file; + } + } + return $history; +} + +/** * Update the {locale_file} table. * * @param $file @@ -756,7 +783,7 @@ function locale_preprocess_node(&$variables) { * @see drupal_write_record() */ // @todo Is this the right place for this function? -function locale_translate_update_file_history($file) { +function locale_translation_update_file_history($file) { // Update or write new record. if (db_query("SELECT project FROM {locale_file} WHERE project = :project AND langcode = :langcode", array(':project' => $file->project, ':langcode' => $file->langcode))->fetchField()) { $update = array('project', 'langcode'); @@ -768,6 +795,18 @@ function locale_translate_update_file_history($file) { } /** + * Delete the history of downloaded translations. + * + * @param string $langcode + * Language code of the file history to be deleted. + */ +function locale_translation_file_history_delete($langcode) { + db_delete('locale_file') + ->condition('language', $langcode) + ->execute(); +} + +/** * Save the status of translation sources in static cache. * * @param array $data diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 8ea8780..2386fdb 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -471,15 +471,142 @@ function locale_translation_manual_status() { * * @see locale_menu() */ -function locale_translation_status() { +function locale_translation_status_form($form, &$form_state) { + module_load_include('compare.inc', 'locale'); + //locale_translation_flush_projects(); + $projects = locale_translation_get_projects(); $languages = locale_translatable_language_list(); + $status = state()->get('locale_translation_status'); + if (!$languages) { drupal_set_message(t('No translatable languages available. Add language first.', array('@add_lanuage' => url('admin/config/regional/language'))), 'warning'); } + // === Start of temporay code. For development only === + $last = state()->get('locale_translation_status_last_update'); + $markup = '

'; + $markup .= $last ? t('Last checked: @time ago', array('@time' => format_interval(REQUEST_TIME - $last))) : t('Last checked: never'); + $markup .= ' (' . l(t('Check manually'), 'admin/reports/translations/check', array('query' => drupal_get_destination())) . ')'; + $markup .= '

'; + $form['last_checked'] = array( + '#markup' => $markup, + ); + + $table_header = array(t('Project'), t('Version'), t('Status'), t('Current version'), t('Timestamp'), t('Recommended version'), t('Timestamp'), t('Source')); + $rows = array(); + foreach ($projects as $name => $project) { + foreach ($languages as $langcode => $language) { + if (isset($status[$name][$langcode])) { + $source = $status[$name][$langcode]; + $row = array( + 'project' => $name, + 'version' => $source->version, + 'status' => isset($source->type) ? $source->type == 'current' ? t('Up to date') : t('Translations available') : t('Unknown'), + 'current version' => '', + 'current timestamp' => '', + 'recommended version' => '', + 'recommended timestamp' => '', + 'recommended source' => '', + ); + if (isset($source->files['current'])) { + $current = $source->files['current']; + $row['current version'] = $current->version; + $row['current timestamp'] = format_date($current->timestamp, 'custom', 'j M - G:i:s'); + } + if ($row['status'] == t('Translations available')) { + $available = $source->files[$source->type]; + $row['recommended version'] = $available->version; + $row['recommended timestamp'] = format_date($available->timestamp, 'custom', 'j M - G:i:s'); + $row['recommended source'] = $available->type; + } + } + else { + $row = array( + 'project' => $project, + 'version' => $source->version, + 'status' => t('Unknown'), + 'current version' => '', + 'current timestamp' => '', + 'recommended version' => '', + 'recommended timestamp' => '', + 'recommended source' => '', + ); + } + $rows[] = $row; + } + } + $form['table'] = array( + '#markup' => theme('table', array('header' => $table_header, 'rows' => $rows)), + ); + // @todo Calculate and display the translation status here. See the follow-up // issue for translation interface: http://drupal.org/node/1804702 - return 'TODO: Show the translation status here'; + // This form content is temporary and required to get the tests working. + $options = array(); + foreach ($projects as $name => $project) { + $options[$name] = $project->name; + } + $form['projects'] = array( + '#title' => t('Projects'), + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => drupal_map_assoc(array_keys($options)), + '#required' => TRUE, + ); + + $options = array(); + foreach ($languages as $langcode => $language) { + $options[$langcode] = $language->name; + } + $form['langcodes'] = array( + '#title' => t('Languages'), + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => drupal_map_assoc(array_keys($options)), + '#required' => TRUE, + ); + + // === End of temporay code. For development only === + + $form['actions'] = array( + '#type' => 'actions' + ); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Update') + ); + return $form; +} + +/** + * Form submission handler for locale_translation_status(). + */ +function locale_translation_status_form_submit($form, &$form_state) { + $langcodes = array_filter($form_state['values']['langcodes']); + $projects = array_filter($form_state['values']['projects']); + + // If the status was updated recently we can immediately start updating. If + // the status is not expired we will get the status first. + $last_checked = state()->get('locale_translation_status_last_update'); + if ($last_checked < REQUEST_TIME - LOCALE_TRANSLATION_STATUS_TTL) { + locale_translation_clear_status(); + // @todo Batch with check status + download + import. + } + else { + module_load_include('fetch.inc', 'locale'); + $status = state()->get('locale_translation_status'); + // Use only the selected projects and languages for update. + $sources = array(); + foreach($projects as $project) { + foreach ($langcodes as $langcode) { + if (isset($status[$project][$langcode])) { + $sources[] = $status[$project][$langcode]; + } + } + } + $batch = locale_translation_batch_fetch_build($sources); + batch_set($batch); + } } /**