diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc new file mode 100644 index 0000000..93a85aa --- /dev/null +++ b/core/modules/locale/locale.batch.inc @@ -0,0 +1,181 @@ + $operations, + 'title' => t('Checking available translations'), + 'finished' => 'locale_translation_batch_status_finished', + 'error_message' => t('Error checking available 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 + * file URL and timestamp are stored. + * + * @param array $source + * A translation source object of the project for which to check the state of + * a remote po file. + * @param array $context + * The batch context array. The collected state is stored in the 'results' + * parameter of the context. +*/ +function locale_translation_batch_status_fetch_remote($source, &$context) { + // Check the translation file at the remote server and update the source + // data with the remote status. + $result = locale_translation_http_check($source->url); + if ($result && !empty($result->updated)) { + + // Modify the source object with the result data. + // There may have been redirects so we store the resulting url. + $source->type = 'remote'; + $source->fileurl = isset($result->redirect_url) ? $result->redirect_url : $source->url; + unset($source->url); + $source->timestamp = $result->updated; + } + + // Store the source data in the context for later processing. + $context['results'][$source->name][$source->language]['remote'] = $source; +} + +/** + * Batch operation callback: Check the availability of local po files. + * + * Checks the presence and creation time of po files in the local file system. + * The file path and the timestamp are stored. + * + * @param array $sources + * Array of translation source objects of projects for which to check the + * state of local po files. + * @param array $context + * The batch context array. The collected state is stored in the 'results' + * parameter of the context. + */ +function locale_translation_batch_status_fetch_local($sources, &$context) { + module_load_include('compare.inc', 'locale'); + + foreach ($sources as $source) { + // Get the status of local translation source and update the source data + // with this. + locale_translation_source_check_file($source); + + // Store the source data in the context for later processing. + $context['results'][$source->name][$source->language]['local'] = $source; + } +} + +/** + * Batch operation callback: Compare states and store the result. + * + * Compare the collected results of local and remote sources and store the most + * recent one. The collected result are stored in the 'results' element of the + * batch context parameter. + * The results are stored in the 'locale_translation_status' state. Other + * processes can collect this data once the batch is completed. + * + * @param array $context + * The batch context array. The 'results' element contains a structured array + * of project data with languages, local and remote source data. + */ +function locale_translation_batch_status_compare(&$context) { + module_load_include('compare.inc', 'locale'); + $results = array(); + + foreach ($context['results'] as $project => $langcodes) { + foreach ($langcodes as $langcode => $sources) { + $local = $sources['local'] ? $sources['local'] : array(); + $remote = $sources['remote']; + if ($result = _locale_translation_source_compare($local, $remote) < 0 ? $remote : $local) { + $results[$project][$langcode] = $result; + } + } + } + + state()->set('locale_translation_status', $results); + state()->set('locale_translation_status_last_update', REQUEST_TIME); +} + +/** + * Batch finished callback: Set result message. + * + * @param boolean $success + * TRUE if batch succesfully completed. + * @param array $results + * Batch results. + */ +function locale_translation_batch_status_finished($success, $results) { + $t = get_t(); + if($success) { + if ($results) { + drupal_set_message(format_plural( + count($results), + 'Checked available interface translation updates for one project.', + 'Checked available interface translation updates for @count projects.' + )); + } + } + else { + drupal_set_message($t('An error occurred trying to check available interface translation updates.'), 'error'); + } +} + +/** + * Check if remote file exists and when it was last updated. + * + * @param string $url + * URL 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. + */ +// @todo Replace this with a stream wrapper? May use ReadOnlyStreamWrapper: +// http://drupal.org/node/1308054 +function locale_translation_http_check($url, $headers = array()) { + $result = drupal_http_request($url, array('headers' => $headers, 'method' => 'HEAD')); + if ($result && $result->code == '200') { + $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0; + } + return $result; +} diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index 039eba3..d219055 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -12,6 +12,15 @@ */ const LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN = 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po'; +/** + * Threshold for timestamp comparison. + * + * Eliminates a difference between the download time and the actual .po file + * timestamp in seconds. The download time is stored in the database in + * {locale_file}.timestamp. + */ +const LOCALE_TRANSLATION_TIMESTAMP_THRESHOLD = 2; + use Drupal\Core\Cache; /** @@ -225,6 +234,10 @@ function _locale_translation_prepare_project_list($data, $type) { if (isset($file->info['interface translation project'])) { $data[$name]->info['project'] = $file->info['interface translation project']; } + // @todo Here we could mark projects which do not have a 'project' and therefore should not + // be checked for a remote translation. Unless explictly provided by 'interface translation server'. + // Use file_uri_scheme() here? + } return $data; } @@ -269,3 +282,220 @@ function locale_translation_build_server_pattern($project, $template) { ); return strtr($template, $variables); } + +/** + * Check for the latest release of project translations. + * + * @param array $projects + * Projects to check (objects). + * @param string $langcodes + * Array of language codes to check for. Leave empty to check all languages. + * + * @return array + * Available sources indexed by project and language. + */ +function locale_translation_check_projects($projects, $langcodes = NULL) { + module_load_include('batch.inc', 'locale'); + + $check_remote = variable_get('locale_translation_check_mode', LOCALE_TRANSLATION_CHECK_ALL) & LOCALE_TRANSLATION_CHECK_REMOTE; + if ($check_remote) { + // Retrieve the status of both remote and local translation sources by + // using a batch process. + locale_translation_check_projects_batch($projects, $langcodes); + } + else { + // Retrieve and save the status of local translations only. + locale_translation_check_projects_local($projects, $langcodes); + } +} + +/** + * Gets and stores the status and creation timestamp of remote po files. + * + * A batch process is used to check for po files at remote locations and (if + * configured) to check for po file in the local file system. The most recent + * translation source states are stored in the state variable + * 'locale_translation_status'. + * + * @params array $projects + * Array of translatable projects. + * @params array $langcodes + * Array of language codes to check for. Leave empty to check all languages. + */ +function locale_translation_check_projects_batch($projects, $langcodes = NULL) { + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $sources = array(); + foreach ($projects as $name => $project) { + foreach ($langcodes as $langcode) { + $project_clone = clone $project; + $source = locale_translation_source_build($project_clone, $langcode); + $source->url = locale_translation_build_server_pattern($source, $source->server_pattern); + $sources[] = $source; + } + } + + // Build and set the batch process. + module_load_include('batch.inc', 'locale'); + $batch = locale_translation_batch_status_build($sources); + batch_set($batch); +} + +/** + * Check and store the status and creation timestamp of local po files. + * + * Only po files in the local file system and checked. Any remote translation + * sources will be ignored. Results are stored in the state variable + * 'locale_translation_status'. + * + * Projects may contain a server_pattern option containing the pattern of the + * path to the po source files. If not defined the default translation directory + * is scanned for presence of po files. If defined the specified location is + * checked. The server_pattern can be set in the module's .info file or by + * using hook_locale_translation_projects_alter(). + * + * The time/date the po file was written (PHP: filemtime()) is used as + * timestamp. + * + * @params array $projects + * Array of translatable projects. + * @params array $langcodes + * Array of language codes to check for. Leave empty to check all languages. + */ +function locale_translation_check_projects_local($projects, $langcodes = NULL) { + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $results = array(); + + foreach ($projects as $name => $project) { + foreach ($langcodes as $langcode) { + $project_clone = clone $project; + $source = locale_translation_source_build($project_clone, $langcode); + if (locale_translation_source_check_file($source)) { + $results[$name][$langcode] = $source; + } + } + } + + state()->set('locale_translation_status', $results); + state()->set('locale_translation_status_last_update', REQUEST_TIME); +} + +/** + * Check whether a po file exists in the filesystem. + * + * It will search in the directory set in the translation source. Which defaults + * to the files/translations directory as set in the variable + * 'locale_translate_file_directory'. + * + * It will search for a file name as set in the translation source. Which + * defaults to LOCALE_TRANSLATION_DEFAULT_FILENAME. Per project this value + * can be overridden using the server_pattern directive in the module's .info + * file or by using hook_locale_translation_projects_alter(). + * + * @param stdClass $source + * Translation file object. + * @see locale_translation_source_build() + * + * @return stdClass + * File object (filename, basename, name) updated with data of the po file: + * - "type": Fixed value 'local'. + * - "uri": File name and path. + * - "timestamp": Last updated time. + * NULL if failure. + */ +// @todo incorporate translations:// streamwrapper. See http://drupal.org/node/1658842 +function locale_translation_source_check_file(&$source) { + $directory = $source->directory; + $filename = '/' . preg_quote($source->filename) . '$/'; + + // Get the time stamp and file date from file from the directory which matches + // the filename. + if ($files = drupal_system_listing($filename, $directory, 'name', 0)) { + $file = current($files); + $source->type = 'local'; + $source->uri = $file->uri; + $source->timestamp = filemtime($file->uri); + return $file; + } + return NULL; +} + +/** + * Build abstract translation source, to be mapped to a file or a download. + * + * @param stdClass $project + * Project object. + * @param string $langcode + * Language code. + * @param string $filename + * File name of translation file. May contains placeholders. + * + * @return object + * Source object, which may have these properties: + * - "project": Project name. + * - "language": Language code. + * - "type": Source type 'remote' or 'local'. + * - "uri": Local file path. + * - "fileurl": Remote file URL for downloads. + * - "directory": Directory of the local po file. + * - "filename": File name. + * - "keep": TRUE to keep the downloaded file. //@todo Is this still applicable? + * - "timestamp": Last update time of the file. + */ +// @todo Move this file? +function locale_translation_source_build($project, $langcode, $filename = LOCALE_TRANSLATION_DEFAULT_FILENAME) { + $source = clone $project; + $source->project = $project->name; + $source->language = $langcode; + + // If the server_pattern contains a remote file path we check for a + // corresponding local po file in the local translation directory. If the server_pattern + // is a local po file path we chekc for a po file using this pattern. + module_load_include('inc', 'file'); + if (file_uri_scheme($source->server_pattern)) { + $source->directory = variable_get('locale_translate_file_directory', conf_path() . '/files/translations'); + $source->filename = locale_translation_build_server_pattern($source, $filename); + } + else { + $source->directory = dirname($source->server_pattern); + $source->filename = locale_translation_build_server_pattern($source, basename($source->server_pattern)); + } + + return $source; +} + +/** + * Compare two update sources, looking for the newer one. + * + * The timestamp property of the source objects are used to determine which is + * the newer one. + * + * @param stdClass $source1 + * Source object of the first translation source. + * @param stdClass $source2 + * Source object of available update. + * + * @return integer + * - "-1": $source1 < $source2 OR $source1 is missing. + * - "0": $source1 == $source2 OR both $source1 and $source2 are missing. + * - "1": $source1 > $source2 OR $source2 is missing. + */ +// @todo Move to locale.inc? +function _locale_translation_source_compare($source1, $source2) { + if ($source1 && $source2) { + if (abs($source1->timestamp - $source2->timestamp) < LOCALE_TRANSLATION_TIMESTAMP_THRESHOLD) { + return 0; + } + else { + return $source1->timestamp > $source2->timestamp ? 1 : -1; + } + } + elseif ($source1 && !$source2) { + return 1; + } + elseif (!$source1 && $source2) { + return -1; + } + else { + return 0; + } +} diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index a32d47d..34f645e 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -66,6 +66,43 @@ const LOCALE_NOT_CUSTOMIZED = 0; */ const LOCALE_CUSTOMIZED = 1; +/** + * Translation update mode: Use remote server only. + * + * When checking for available translation updates, only remote servers will be + * used. Any local translation file will be ignored. Custom modules and themes + * which have set a "server pattern" to use local translation files will still + * be checked. + */ +const LOCALE_TRANSLATION_CHECK_REMOTE = 1; + +/** + * Translation update mode: Use local files only. + * + * When checking for available translation updates, only local files will be + * used. Any local remote server will be ignored. Also custom modules and themes + * which have set a "server pattern" to use a remote translation server will be + * ignored. + */ +const LOCALE_TRANSLATION_CHECK_LOCAL = 2; + +/** + * Translation update mode: Use both remote and local files. + * + * When checking for available translation updates, both local and remote files + * will be checked. + */ +define('LOCALE_TRANSLATION_CHECK_ALL', LOCALE_TRANSLATION_CHECK_REMOTE | LOCALE_TRANSLATION_CHECK_LOCAL); + +/** + * Default file name of translation files stored in the local file system. + * + * The file name containing placeholders which are also used by the server + * pattern. See locale_translation_build_server_pattern() for supported placeholders. + */ +const LOCALE_TRANSLATION_DEFAULT_FILENAME = '%project-%version.%language.po'; + + // --------------------------------------------------------------------------------- // Hook implementations @@ -230,6 +267,26 @@ function locale_language_delete($language) { cache()->delete('locale:' . $language->langcode); } +// --------------------------------------------------------------------------------- +// Locale translation core functionality + +/** + * Returns list of translatable languages. + * + * @return array + * Array of installed languages keyed by language name. English is omitted + * unless its marked as translatable. + */ +function locale_translatable_language_list() { +//drupal_static_reset('language_list'); + $languages = language_list(); +//debug($languages); +//debug(db_query('SELECT * FROM {language} ORDER BY weight ASC, name ASC')->fetchAllAssoc('langcode', PDO::FETCH_ASSOC)); + if (!locale_translate_english()) { + unset($languages['en']); + } + return $languages; +} // --------------------------------------------------------------------------------- // Locale core functionality @@ -811,10 +868,7 @@ function _locale_invalidate_js($langcode = NULL) { if (empty($langcode)) { // Invalidate all languages. - $languages = language_list(); - if (!locale_translate_english()) { - unset($languages['en']); - } + $languages = locale_translatable_language_list(); foreach ($languages as $lcode => $data) { $parsed['refresh:' . $lcode] = 'waiting'; }