diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc new file mode 100644 index 0000000..d3393ea --- /dev/null +++ b/core/modules/locale/locale.batch.inc @@ -0,0 +1,117 @@ + $operations, + 'title' => t('Checking available translation update data'), + 'finished' => 'locale_translation_batch_status_finished', + 'error_message' => t('Error checking available translation updates.'), + 'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc', + ); + batch_set($batch); +} + +/** + * Batch job: Fetches status of single po file at a translation server. + * + * @todo + */ +function locale_translation_batch_status_fetch($source, &$context) { + $t = get_t(); + $cid = 'available_translations_remote:' . $source->url; + + // Check the translation file at the remote server and store the result in + // cache. + $result = locale_translation_http_check($source->url); + if ($result && !empty($result->updated)) { + $context['message'] = $t('Checking available translation update data ...'); + + // Modify the source object with the result data. + // There may have been redirects so we store the resulting url. + $source->type = 'download'; + $source->fileurl = isset($result->redirect_url) ? $result->redirect_url : $source->url; + unset($source->url); + $source->timestamp = $result->updated; + + // Store the data in cache. + // @todo Is $source->url the right key to use? Alternative $source->name + $source->language + $frequency = variable_get('locale_translation_check_frequency', 0); + $expire = $frequency ? REQUEST_TIME + (60 * 60 * 24 * $frequency) : REQUEST_TIME + LOCALE_TRANSLATION_CACHE_MIN_TTL; + cache('locale')->set($cid, $source, $expire); + + $context['results'] ++; + } + + // If no file was found on the server, the project is a custom module or + // no translation has yet been released by the translation server. + // We cache this unchanged with a minimum life time to prevent requesting too + // often. + $expire = REQUEST_TIME + LOCALE_TRANSLATION_CACHE_MIN_TTL; + cache('locale')->set($cid, $source, $expire); +} + +/** + * @todo + */ +function locale_translation_batch_status_finished($success, $results) { + $t = get_t(); + if($success) { + if ($results) { + drupal_set_message(format_plural( + $results['updated'], + '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 $url + * URL of remote file. + * @param $headers + * HTTP request headers. + * @return object + * 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 new file mode 100644 index 0000000..7d300d2 --- /dev/null +++ b/core/modules/locale/locale.compare.inc @@ -0,0 +1,135 @@ + variable_get('locale_translation_default_server', LOCALE_TRANSLATION_DEFAULT_SERVER), + 'server_url' => variable_get('locale_translation_default_server_url', LOCALE_TRANSLATION_DEFAULT_SERVER_URL), + 'server_pattern' => variable_get('locale_translation_default_server_pattern', LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN), + ); +} + +/** + * Build abstract translation source, to be mapped to a file or a download. + * + * @param $project + * Project object containing data to be inserted in the template. + * @param $template + * String containing place holders. Available place holders: + * - '%project': Project name. + * - '%release': Poject version. + * - '%core': Project core version. + * - '%language': Language code. + * - '%filename': Project file name. + * @return string + * String with replaced place holders. + */ +/* @todo Based on l10n_update_build_string(). */ +/* @todo Move to locale.inc? */ +function locale_translation_build_string($project, $template) { + $variables = array( + '%project' => $project->name, + '%version' => $project->version, + '%core' => $project->core, + '%language' => isset($project->language) ? $project->language : '%language', + '%filename' => isset($project->filename) ? $project->filename : '%filename', + ); + return strtr($template, $variables); +} + +/** + * Build abstract translation source, to be mapped to a file or a download. + * + * @param $project + * Project object. + * @param $langcode + * Language code. + * @param $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 'download' or 'localfile'. + * - 'uri': Local file path. + * - 'fileurl': Remote file URL for downloads. + * - 'filename': File name. + * - 'keep': TRUE to keep the downloaded file. + * - 'timestamp': Last update time of the file. + */ +/* @todo Based on l10n_update_source_build(). */ +/* @todo Move to which file? */ +function locale_translation_source_build($project, $langcode, $filename = LOCALE_TRANSLATION_DEFAULT_FILENAME) { + $source = clone $project; + $source->project = $project->name; + $source->language = $langcode; + $source->filename = locale_translation_build_string($source, $filename); + return $source; +} + +/** + * @todo + */ +function locale_translation_check_projects_remote($projects, $languages = NULL) { + $languages = $languages ? $languages : array_keys(locale_translatable_language_list()); + $sources = array(); + foreach ($projects as $name => $project) { + foreach ($languages as $lang) { + $project_clone = clone $project; + $source = locale_translation_source_build($project_clone, $lang); + $source->url = locale_translation_build_string($source, $source->server_pattern); + $sources[] = $source; + } + } + return locale_translation_get_remote_status($sources); +} + +/** + * @todo + */ +function locale_translation_get_remote_status($sources) { + $status = array(); + + // If the status is in cache and is not expired, + // don't bother checking at the server. + $batch_sources = $sources; + foreach($batch_sources as $name => $source) { + $cid = 'available_translations_remote:' . $source->url; + $cache = cache('locale')->get($cid); + if ($cache && $cache->expire > REQUEST_TIME) { + unset($batch_sources[$name]); + } + } + + // Build and execute the batch process. + module_load_include('batch.inc', 'locale'); + locale_translation_batch_status($batch_sources); + + // Get the result from cache and return the data. + foreach ($sources as $source) { + $cid = 'available_translations_remote:' . $source->url; + if ($cache = cache('locale')->get($cid)) { + // @todo: Check if usable data is available if the file was not found at the server. + $status[$cache->data->project][$cache->data->language] = $cache->data; + } + } +//debug($status); + return $status; +} diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 525a339..5f09109 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -12,6 +12,7 @@ */ use Drupal\locale\LocaleLookup; +use Drupal\Core\Cache; /** * Regular expression pattern used to localize JavaScript strings. @@ -64,6 +65,36 @@ const LOCALE_NOT_CUSTOMIZED = 0; */ const LOCALE_CUSTOMIZED = 1; +/** + * Default translation server name. + * + * @see locale_translation_default_translation_server(). + */ +const LOCALE_TRANSLATION_DEFAULT_SERVER = "localize.drupal.org"; + +/** + * Default URL of the xml file at the translation server containing all + * available languages. + * + * @see locale_translation_default_translation_server(). + */ +const LOCALE_TRANSLATION_DEFAULT_SERVER_URL = 'http://localize.drupal.org/l10n_server.xml'; + +/** + * Default pattern of path and name of the gettext file at the translation + * server. + * + * @see locale_translation_default_translation_server(). + */ +const LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN = 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po'; + +/** + * Default translation filename pattern. Used just for local po file imports. + */ +const LOCALE_TRANSLATION_DEFAULT_FILENAME = '%project-%release.%language.po'; + + + // --------------------------------------------------------------------------------- // Hook implementations @@ -213,6 +244,129 @@ function locale_language_delete($language) { cache()->delete('locale:' . $language->langcode); } +/** + * Implements hook_locale_translation_servers(). + * + * @return array + * Array of server data: + * 'name' => server name + * 'server_url' => server url + * 'update_url' => update url + */ +// @todo Based on l10n_update_l10n_servers(). +function locale_locale_translation_servers() { + // @todo move locale_translation_default_translation_server from locale.compare.inc to locale.translation.inc? + module_load_include('compare.inc', 'locale'); + $server = locale_translation_default_translation_server(); + return array($server['name'] => $server); +} + +// --------------------------------------------------------------------------------- +// Locale translation core functionality + +/** + * Get server information, that can come from different sources. + * + * - From server list provided by modules. They can provide full server + * information or just the url. + * - From server_url in a project. We fetch the latest data from the server + * itself. + * + * @param string $name + * Server name e.g. localize.drupal.org + * @param string $url + * Server url + * @param boolean $refresh + * TRUE = refresh the server data. + * + * @return array + * Array of server data. + */ +// @todo Based on l10n_update_server(). +function locale_translation_server($name = NULL, $url = NULL, $refresh = FALSE) { + // @todo Do we need the flexibility that the server configuration can retrieved from the remote server? + // @todo We can only replace $refresh with locale_reset_update_server() if we make the server $name a required variable. + $info = &drupal_static(__FUNCTION__ . ':info'); + $server_list = &drupal_static(__FUNCTION__ . ':list'); + + // Retrieve server list from modules. + if (!isset($server_list) || $refresh) { + $server_list = module_invoke_all('locale_translation_servers'); + } + // We need at least the server url to fetch all the information. + if (!$url && $name && isset($server_list[$name])) { + $url = $server_list[$name]['server_url']; + } + // If we still don't have an url, cannot find this server, return false. + if (!$url) { + return FALSE; + } + // Cache server information based on the url, refresh if asked. + // @todo Replace $url by $name if $name becomes required. + $cid = 'translation_server:' . $url; + if ($refresh) { + unset($info); + cache('locale')->delete($cid); + } + if (!isset($info[$url])) { + if ($cache = cache('locale')->get($cid)) { + $info[$url] = $cache->data; + } + else { + if ($name && !empty($server_list[$name])) { + // The name is in our list, it can be full data or just an url. + $server = $server_list[$name]; + } + else { + // This may be a new server provided by a module / package. + $server = array('name' => $name, 'server_url' => $url); + // If searching by name, store the name => url mapping. + if ($name) { + $server_list[$name] = $server; + } + } + // Now fetch server meta information form the server itself + // @todo Note: This function is included in issue http://drupal.org/node/1632384 + module_load_include('fetch.inc', 'locale'); + if ($server = locale_translation_get_server($server)) { + cache('locale')->set($cid, $server); + $info[$url] = $server; + } + else { + // If no server information, this will be FALSE. We won't search a + // server twice. + $info[$url] = FALSE; + } + } + } + return $info[$url]; +} + +/** + * @todo + */ +// @todo We can only use this function if we make the server $name in locale_translation_server() a required variable. +function locale_reset_translation_server($name) { + drupal_static_reset('locale_translation_server:info'); + drupal_static_reset('locale_translation_server:list'); + $cid = 'translation_server:' . $name; + cache('locale')->delete($cid); +} + +/** + * 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() { + $languages = language_list(); + if (!locale_translate_english()) { + unset($languages['en']); + } + return $languages; +} // --------------------------------------------------------------------------------- // Locale core functionality @@ -746,10 +900,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'; } diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc new file mode 100644 index 0000000..f566204 --- /dev/null +++ b/core/modules/locale/locale.translation.inc @@ -0,0 +1,7 @@ +