diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php index 97af1fc..9b8432f 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php @@ -45,7 +45,11 @@ function setUp() { } /** - * @todo + * Set the value of the default translations directory. + * + * @param string $path + * Path of the translations directory relative to the drupal installation + * directory. */ private function setTranslationsDirectory($path) { $this->tranlations_directory = $path; @@ -54,7 +58,16 @@ private function setTranslationsDirectory($path) { } /** - * @todo + * Creates a translation file and test its timestamp. + * + * @param string $path + * Path of the file relative to the public file path. + * @param string $filename + * Name of the file to create. + * @param string $timestamp + * Timestamp to set the file to. Defaults to current time. + * @param string $data + * Translation data to put into the file. Po header data will be added. */ private function makePoFile($path, $filename, $timestamp = NULL, $data = '') { $timestamp = $timestamp ? $timestamp : REQUEST_TIME; @@ -135,7 +148,25 @@ function testLocaleCompare() { /** * Checks if local or remote translation sources are detected. * - * @todo Describe the test strategy. + * This test requires a simulated environment for local and remote files. + * Normally remote files are located at a remote server (e.g. ftp.drupal.org). + * For testing we can not rely on this. A directory in the file system of the + * test site is designated for remote files and is addressed using an absolute + * URL. Because drupal does not allow file with a po extension to be accessed + * (denied in .htaccess) the translation files get a txt extension. An other + * directory is designated for local translation files. + * + * The translation status process by default checks the status of the + * installed projects. For testing purpose a predefined set of modules with + * fixed file names and release versions is used. Using a + * hook_locale_translation_projects_alter implementation in the locale_test + * module this custom project definition is applied. + * + * This test generates a set of local and remote translation files in their + * respective local and remote translation directory. The test check whether + * the most recent files are selected in the different check scenario's: check + * for local files only, check for remote files only, check for both local and + * remote files. */ function testCompareCheckLocal() { $config = config('locale.settings'); @@ -165,6 +196,7 @@ function testCompareCheckLocal() { // locale_test_locale_translation_projects_alter(). $this->makePoFile('local', 'contrib_module_one-8.x-1.1.de.txt', $timestamp_old); $this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de.txt', $timestamp_new); + $this->makePoFile('local', 'custom_module_one.de.po', $timestamp_new); //debug(file_scan_directory(variable_get('file_public_path', conf_path() . '/files') . '/local', '/.*\.*/', array('recurse' => TRUE))); //debug(file_scan_directory(variable_get('file_public_path', conf_path() . '/files') . '/remote', '/.*\.*/', array('recurse' => TRUE))); @@ -188,7 +220,8 @@ function testCompareCheckLocal() { $this->assertEqual($result['contrib_module_one']['de']->timestamp, $timestamp_old, 'Translation timestamp found'); $this->assertEqual($result['contrib_module_two']['de']->type, 'local', 'Translation of contrib_module_two found'); $this->assertEqual($result['contrib_module_two']['de']->timestamp, $timestamp_new, 'Translation timestamp found'); - $this->assertEqual($result['locale_test']['de']->type, 'local', 'Translation of test.de.po found'); + $this->assertEqual($result['locale_test']['de']->type, 'local', 'Translation of locale_test found'); + $this->assertEqual($result['custom_module_one']['de']->type, 'local', 'Translation of custom_module_one found'); // Get status of translation sources at both local and remote the locations. $config->set('translation.check_mode', LOCALE_TRANSLATION_CHECK_ALL)->save(); @@ -200,6 +233,7 @@ function testCompareCheckLocal() { $this->assertEqual($result['contrib_module_two']['de']->timestamp, $timestamp_new, 'Translation timestamp found'); $this->assertEqual($result['contrib_module_three']['de']->type, 'remote', 'Translation of contrib_module_three found'); $this->assertEqual($result['contrib_module_three']['de']->timestamp, $timestamp_old, 'Translation timestamp found'); - $this->assertEqual($result['locale_test']['de']->type, 'local', 'Translation of test.de.po found'); + $this->assertEqual($result['locale_test']['de']->type, 'local', 'Translation of locale_test found'); + $this->assertEqual($result['custom_module_one']['de']->type, 'local', 'Translation of custom_module_one. found'); } } diff --git a/core/modules/locale/locale.api.php b/core/modules/locale/locale.api.php index 91b9cda..ea1085f 100644 --- a/core/modules/locale/locale.api.php +++ b/core/modules/locale/locale.api.php @@ -28,7 +28,18 @@ * the module's folder. * @code * interface translation project = example_module - * interface translation server pattern = sites/example.com/modules/custom/example_module/%project-%version.%language.po + * interface translation server pattern = sites/all/modules/custom/example_module/%project-%version.%language.po + * @endcode + * + * Streamwrappers can be used in the server pattern definition. The interface + * translations directory (Configuration > Media > File system) can be addressed + * using the "translations://" streamwrapper. But also other streamwrappers can + * be used. + * @code + * interface translation server pattern = translations://%project-%version.%language.po + * @endcode + * @code + * interface translation server pattern = public://translations/%project-%version.%language.po * @endcode * * Multiple custom modules or themes sharing the same po file should have diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc index c6e30a2..adfebf5 100644 --- a/core/modules/locale/locale.batch.inc +++ b/core/modules/locale/locale.batch.inc @@ -2,7 +2,7 @@ /** * @file - * @todo + * Batch proces to check the availability of remote Gettext .po files. */ /** @@ -26,22 +26,16 @@ function locale_translation_batch_status_build($sources) { $operations[] = array('locale_translation_batch_status_fetch_remote', array($source)); } - // Only fetch local data if it's configured to do so. - // @todo We should make an exception for modules without remote source. - // But this requires a flag per project to be set when the .info data is parsed. - $check_mode = config('locale.settings')->get('translation.check_mode'); - if ($check_mode == LOCALE_TRANSLATION_CHECK_ALL || $check_mode == LOCALE_TRANSLATION_CHECK_LOCAL) { - $operations[] = array('locale_translation_batch_status_fetch_local', array($sources)); - } - - // Compare results of local and remote and store the most recent. + // 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 translation updates.'), + 'error_message' => t('Error checking available interface translation updates.'), 'file' => drupal_get_path('module', 'locale') . '/locale.batch.inc', ); return $batch; @@ -63,19 +57,19 @@ function locale_translation_batch_status_build($sources) { 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; + if (isset($source->files['remote'])) { + $remote_file = $source->files['remote']; + $result = locale_translation_http_check($remote_file->url); + + // Update the file object with the result data. In case of a redirect we + // we store the resulting url. + if ($result && !empty($result->updated)) { + $remote_file->url = isset($result->redirect_url) ? $result->redirect_url : $remote_file->url; + $remote_file->timestamp = $result->updated; + $source->files['remote'] = $remote_file; + } + $context['results'][$source->name][$source->language] = $source; } - - // Store the source data in the context for later processing. - $context['results'][$source->name][$source->language]['remote'] = $source; } /** @@ -84,6 +78,11 @@ function locale_translation_batch_status_fetch_remote($source, &$context) { * Checks the presence and creation time of po files in the local file system. * The file path and the timestamp are stored. * + * We check local files if the site is configured to check local or both local + * and remote files. But also we also check local sources if no remote file is + * defined for this source. For example for custom modules which + * bring their own po file and don't have a remote translation file. + * * @param array $sources * Array of translation source objects of projects for which to check the * state of local po files. @@ -93,25 +92,36 @@ function locale_translation_batch_status_fetch_remote($source, &$context) { */ function locale_translation_batch_status_fetch_local($sources, &$context) { module_load_include('compare.inc', 'locale'); + $check_mode = config('locale.settings')->get('translation.check_mode'); + $check_local = $check_mode == LOCALE_TRANSLATION_CHECK_ALL || $check_mode == LOCALE_TRANSLATION_CHECK_LOCAL; + $check_remote = $check_mode == LOCALE_TRANSLATION_CHECK_REMOTE; + // Get the status of local translation files and store the result data in the + // batch results for later processing. foreach ($sources as $source) { - // Get the status of local translation source and update the source data - // with this. - locale_translation_source_check_file($source); + if (isset($source->files['local'])) { + if ($check_local || ($check_remote && !isset($source->files['remote']))) { + locale_translation_source_check_file($source); + } - // Store the source data in the context for later processing. - $context['results'][$source->name][$source->language]['local'] = $source; + // If remote data was collected before, we merge it into the newly + // collected result. + if (isset($context['results'][$source->name][$source->language])) { + $source->files['remote'] = $context['results'][$source->name][$source->language]->files['remote']; + } + $context['results'][$source->name][$source->language] = $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. + * In the preceding batch processes data of remote and local translation sources + * is collected. Here we compare the collected results and update the source + * object with the data of the most recent translation file. The end result is + * stored in the 'locale_translation_status' state variable. Other + * processes can collect this data after the batch process is completed. * * @param array $context * The batch context array. The 'results' element contains a structured array @@ -122,11 +132,17 @@ function locale_translation_batch_status_compare(&$context) { $results = array(); foreach ($context['results'] as $project => $langcodes) { - foreach ($langcodes as $langcode => $sources) { - $local = isset($sources['local']) ? $sources['local'] : NULL; - $remote = $sources['remote']; - if ($result = _locale_translation_source_compare($local, $remote) < 0 ? $remote : $local) { - $results[$project][$langcode] = $result; + foreach ($langcodes as $langcode => $source) { + $local = isset($source->files['local']) ? $source->files['local'] : NULL; + $remote = isset($source->files['remote']) ? $source->files['remote'] : NULL; + + // The available translation files are compare and data of the most recent + // file is used to update the source object. + $file = _locale_translation_source_compare($local, $remote) < 0 ? $remote : $local; + if (isset($file->timestamp)) { + $source->type = $file->type; + $source->timestamp = $file->timestamp; + $results[$project][$langcode] = $source; } } } @@ -170,8 +186,6 @@ function locale_translation_batch_status_finished($success, $results) { * 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') { diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index 92b6238..a2c907a 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -227,10 +227,6 @@ 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; } @@ -260,7 +256,6 @@ function locale_translation_default_translation_server() { * - "%version": Project version. * - "%core": Project core version. * - "%language": Language code. - * - "%filename": Project file name. * * @return string * String with replaced placeholders. @@ -271,7 +266,6 @@ function locale_translation_build_server_pattern($project, $template) { '%version' => $project->version, '%core' => $project->core, '%language' => isset($project->language) ? $project->language : '%language', - '%filename' => isset($project->filename) ? $project->filename : '%filename', ); return strtr($template, $variables); } @@ -303,10 +297,10 @@ function locale_translation_check_projects($projects, $langcodes = NULL) { } /** - * Gets and stores the status and creation timestamp of remote po files. + * Gets and stores the status and 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 + * A batch process is used to check for po files at remote locations and (when + * configured) to check for po files in the local file system. The most recent * translation source states are stored in the state variable * 'locale_translation_status'. * @@ -322,7 +316,6 @@ function locale_translation_check_projects_batch($projects, $langcodes = NULL) { 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; } } @@ -334,20 +327,17 @@ function locale_translation_check_projects_batch($projects, $langcodes = NULL) { } /** - * Check and store the status and creation timestamp of local po files. + * Check and store the status and 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. + * Projects may contain a server_pattern option containing a pattern of the + * path to the po source files. If no server_pattern is defined the default + * translation directory is checked for the po file. When a server_pattern is + * 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(). * * @params array $projects * Array of translatable projects. @@ -358,11 +348,15 @@ function locale_translation_check_projects_local($projects, $langcodes = NULL) { $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); $results = array(); + // For each project and each language we check if a local po file is + // available. When found the source object is updated with the appropriate + // type and timestamp of the po file. foreach ($projects as $name => $project) { foreach ($langcodes as $langcode) { - $project_clone = clone $project; - $source = locale_translation_source_build($project_clone, $langcode); + $source = locale_translation_source_build($project, $langcode); if (locale_translation_source_check_file($source)) { + $source->type = 'local'; + $source->timestamp = $source->files['local']->timestamp; $results[$name][$langcode] = $source; } } @@ -373,47 +367,53 @@ function locale_translation_check_projects_local($projects, $langcodes = NULL) { } /** - * Check whether a po file exists in the filesystem. + * Check whether a po file exists in the local 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'. + * to the "translations://" stream wrapper path. The directory may contain any + * valid stream wrapper. * - * It will search for a file name as set in the translation source. Which - * defaults to LOCALE_TRANSLATION_DEFAULT_FILENAME. Per project this value + * The "local" files property of the source object contains the definition of a + * po file we are looking for. The file name 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. + * Translation source object. * @see locale_translation_source_build() * * @return stdClass - * File object (filename, basename, name) updated with data of the po file: - * - "type": Fixed value 'local'. + * File object (filename, basename, name) updated with data of the po file. + * On success the files property of the source object is updated. + * files['local']: * - "uri": File name and path. - * - "timestamp": Last updated time. - * NULL if failure. + * - "timestamp": Last updated time of the po file. + * FALSE if the file is not found. */ -// @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 = file_scan_directory($directory, $filename, array('key' => 'name'))) { - $file = current($files); - $source->type = 'local'; - $source->uri = $file->uri; - $source->timestamp = filemtime($file->uri); - return $file; + if (isset($source->files['local'])) { + $directory = $source->files['local']->directory; + $filename = '/' . preg_quote($source->files['local']->filename) . '$/'; + + // If the directory contains a streamwrapper, it is converted to a real + // path. + if ($scheme = file_uri_scheme($directory)) { + $directory = str_replace($scheme . '://', drupal_realpath($scheme . '://'), $directory); + } + + if ($files = file_scan_directory($directory, $filename, array('key' => 'name'))) { + $file = current($files); + $source->files['local']->uri = $file->uri; + $source->files['local']->timestamp = filemtime($file->uri); + return $file; + } } - return NULL; + return FALSE; } /** - * Build abstract translation source, to be mapped to a file or a download. + * Build abstract translation source. * * @param stdClass $project * Project object. @@ -423,16 +423,26 @@ function locale_translation_source_check_file(&$source) { * File name of translation file. May contains placeholders. * * @return object - * Source object, which may have these properties: + * Source object: * - "project": Project name. + * - "name": Project name (inherited from project). * - "language": Language code. - * - "type": Source type 'remote' or 'local'. + * - "core": Core version (inherited from project). + * - "version": Project version (inherited from project). + * - "project_type": Project type (inherited from project). + * - "files": Array of file objects containing properties of local and remote + * translation files. + * Other processes can add the following properties: + * - "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. - * - "fileurl": Remote file URL for downloads. + * - "url": 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. + * - "timestamp": Timestamp of the file. + * - "keep": TRUE to keep the downloaded file. */ // @todo Move this file? function locale_translation_source_build($project, $langcode, $filename = NULL) { @@ -442,24 +452,52 @@ function locale_translation_source_build($project, $langcode, $filename = NULL) $filename = $filename ? $filename : config('locale.settings')->get('translation.default_filename'); - // 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 check 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); + // If the server_pattern contains a remote file path we will check both + // a remote and a local file. If the server_pattern is a local file path + // we will only check for a local file. + $files = array(); + if (_locale_translation_file_is_remote($source->server_pattern)) { + $files['remote'] = (object) array( + 'type' => 'remote', + 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)), + 'url' => locale_translation_build_server_pattern($source, $source->server_pattern), + ); + $files['local'] = (object) array( + 'type' => 'local', + 'directory' => 'translations://', + '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)); + $files['local'] = (object) array( + '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)), + ); } + $source->files = $files; return $source; } /** + * Determine if the source file is a remote or a local file. + * + * @param string $url + * The URL or URL pattern of the file. + * + * @return boolean + * TRUE if the $url is a remote file location. + */ +function _locale_translation_file_is_remote($url) { + $scheme = file_uri_scheme($url); + if ($scheme) { + return !drupal_realpath($scheme . '://'); + } + return FALSE; +} + +/** * Compare two update sources, looking for the newer one. * * The timestamp property of the source objects are used to determine which is diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 645e79a..b110016 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -16,7 +16,6 @@ use Drupal\locale\StringDatabaseStorage; use Drupal\locale\TranslationsStream; use Drupal\Core\Database\Database; -use Drupal\Core\Cache; /** * Regular expression pattern used to localize JavaScript strings. @@ -113,10 +112,6 @@ */ const LOCALE_TRANSLATION_DEFAULT_FILENAME = '%project-%version.%language.po'; - -// --------------------------------------------------------------------------------- -// Hook implementations - /** * Implements hook_help(). */ @@ -290,9 +285,6 @@ function locale_language_delete($language) { cache()->delete('locale:' . $language->langcode); } -// --------------------------------------------------------------------------------- -// Locale translation core functionality - /** * Returns list of translatable languages. * @@ -301,7 +293,6 @@ function locale_language_delete($language) { * unless its marked as translatable. */ function locale_translatable_language_list() { -//drupal_static_reset('language_list'); $languages = language_list(); if (!locale_translate_english()) { unset($languages['en']); @@ -309,9 +300,6 @@ function locale_translatable_language_list() { return $languages; } -// --------------------------------------------------------------------------------- -// Locale core functionality - /** * Provides interface translation services. * diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.module b/core/modules/locale/tests/modules/locale_test/locale_test.module index 0fa485f..c22e307 100644 --- a/core/modules/locale/tests/modules/locale_test/locale_test.module +++ b/core/modules/locale/tests/modules/locale_test/locale_test.module @@ -27,7 +27,14 @@ function locale_test_system_info_alter(&$info, $file, $type) { /** * Implements hook_locale_translation_projects_alter(). * - * @todo Describe what role this alter plays in the test. + * The translation status process by default checks the status of the installed + * projects. This function replaces the data of the installed modules by a + * predefined set of modules with fixed file names and release versions. Project + * names, versions, timestamps etc must be fixed because they must match the + * files created by the test script. + * + * The "locale_translation_test_projects" state variable must be set by the + * test script in order for this hook to take effect. */ function locale_test_locale_translation_projects_alter(&$projects) { if (state()->get('locale_translation_test_projects')) { @@ -109,7 +116,23 @@ function locale_test_locale_translation_projects_alter(&$projects) { 'interface translation server pattern' => 'core/modules/locale/tests/test.%language.po', 'package' => 'Other', 'version' => NULL, - 'project' => 'ttest', + 'project' => 'locale_test', + '_info_file_ctime' => 1348767306, + 'datestamp' => 0, + ), + 'datestamp' => 0, + 'project_type' => 'module', + 'project_status' => TRUE, + ), + 'custom_module_one' => array ( + 'name' => 'custom_module_one', + 'info' => array ( + 'name' => 'Custom module one', + 'interface translation project' => 'custom_module_one', + 'interface translation server pattern' => 'translations://custom_module_one.%language.po', + 'package' => 'Other', + 'version' => NULL, + 'project' => 'custom_module_one', '_info_file_ctime' => 1348767306, 'datestamp' => 0, ),