diff --git a/core/modules/locale/config/locale.settings.yml b/core/modules/locale/config/locale.settings.yml index d8ba738..c5ddd67 100644 --- a/core/modules/locale/config/locale.settings.yml +++ b/core/modules/locale/config/locale.settings.yml @@ -5,4 +5,5 @@ translation: default_server_pattern: 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po' overwrite_customized: false overwrite_not_customized: true + update_interval_days: '0' diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php deleted file mode 100644 index 017e2c5..0000000 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php +++ /dev/null @@ -1,234 +0,0 @@ - 'Compare project states', - 'description' => 'Tests for comparing status of existing project translations with available translations.', - 'group' => 'Locale', - ); - } - - /** - * Setup the test environment. - * - * We use German as default test language. Due to hardcoded configurations in - * the locale_test module, the language can not be chosen randomly. - */ - function setUp() { - parent::setUp(); - module_load_include('compare.inc', 'locale'); - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'translate interface')); - $this->drupalLogin($admin_user); - $this->drupalPost('admin/config/regional/language/add', array('predefined_langcode' => 'de'), t('Add language')); - } - - /** - * 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; - file_prepare_directory($path, FILE_CREATE_DIRECTORY); - variable_set('locale_translate_file_directory', $path); - } - - /** - * Creates a translation file and tests 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 integer $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; - $path = 'public://' . $path; - $po_header = << 1);\\n" - -EOF; - - file_prepare_directory($path, FILE_CREATE_DIRECTORY); - $file = entity_create('file', array( - 'uid' => 1, - 'filename' => $filename, - 'uri' => $path . '/' . $filename, - 'filemime' => 'text/x-gettext-translation', - 'timestamp' => $timestamp, - 'status' => FILE_STATUS_PERMANENT, - )); - file_put_contents($file->uri, $po_header . $data); - touch(drupal_realpath($file->uri), $timestamp); - $file->save(); - } - - /** - * Test for translation status storage and translation status comparison. - */ - function testLocaleCompare() { - // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages')); - $this->drupalLogin($admin_user); - - module_load_include('compare.inc', 'locale'); - - // Check if hidden modules are not included. - $projects = locale_translation_project_list(); - $this->assertFalse(isset($projects['locale_test']), 'Hidden module not found'); - - // Make the test modules look like a normal custom module. i.e. make the - // modules not hidden. locale_test_system_info_alter() modifies the project - // info of the locale_test and locale_test_disabled modules. - state()->set('locale_translation_test_system_info_alter', TRUE); - - // Reset static system list caches to reflect info changes. - drupal_static_reset('locale_translation_project_list'); - system_list_reset(); - - // Check if interface translation data is collected from hook_info. - $projects = locale_translation_project_list(); - $this->assertEqual($projects['locale_test']['info']['interface translation server pattern'], 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.'); - $this->assertEqual($projects['locale_test']['name'] , 'locale_test', format_string('%key found in project info.', array('%key' => 'interface translation project'))); - - // Get the locale settings. - $config = config('locale.settings'); - - // Check if disabled modules are detected. - $config->set('translation.check_disabled_modules', TRUE)->save(); - drupal_static_reset('locale_translation_project_list'); - $projects = locale_translation_project_list(); - $this->assertTrue(isset($projects['locale_test_disabled']), 'Disabled module found'); - - // Check the fully processed list of project data of both enabled and - // disabled modules. - $config->set('translation.check_disabled_modules', TRUE)->save(); - drupal_static_reset('locale_translation_project_list'); - $projects = locale_translation_get_projects(); - $this->assertEqual($projects['drupal']->name, 'drupal', 'Core project found'); - $this->assertEqual($projects['locale_test']->server_pattern, 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.'); - $this->assertEqual($projects['locale_test_disabled']->status, '0', 'Disabled module found'); - $config->delete('translation.check_disabled_modules'); - - // Return the locale test modules back to their hidden state. - state()->delete('locale_translation_test_system_info_alter'); - } - - /** - * Checks if local or remote translation sources are detected. - * - * 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 files with a po extension to be accessed - * (denied in .htaccess) the translation files get a txt extension. Another - * 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 checks whether - * the most recent files are selected in the different check scenarios: check - * for local files only, check for remote files only, check for both local and - * remote files. - */ - function testCompareCheckLocal() { - $config = config('locale.settings'); - - // A flag is set to let the locale_test module replace the project data with - // a set of test projects. - state()->set('locale_translation_test_projects', TRUE); - - // Setup timestamps to identify old and new translation sources. - $timestamp_old = REQUEST_TIME - 100; - $timestamp_new = REQUEST_TIME; - - // Set up the environment. - $public_path = variable_get('file_public_path', conf_path() . '/files'); - $this->setTranslationsDirectory($public_path . '/local'); - $config->set('translation.default_filename', '%project-%version.%language.txt')->save(); - - // Add a number of files to the local file system to serve as remote - // translation server and match the project definitions set in - // locale_test_locale_translation_projects_alter(). - $this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de.txt', $timestamp_new); - $this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de.txt', $timestamp_old); - $this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de.txt', $timestamp_old); - - // Add a number of files to the local file system to serve as local - // translation files and match the project definitions set in - // 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); - - // Get status of translation sources at local file system. - $config->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)->save(); - $this->drupalGet('admin/reports/translations/check'); - $result = state()->get('locale_translation_status'); - $this->assertEqual($result['contrib_module_one']['de']->type, 'local', 'Translation of contrib_module_one found'); - $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 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.use_source', LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL)->save(); - $this->drupalGet('admin/reports/translations/check'); - $result = state()->get('locale_translation_status'); - $this->assertEqual($result['contrib_module_one']['de']->type, 'remote', 'Translation of contrib_module_one found'); - $this->assertEqual($result['contrib_module_one']['de']->timestamp, $timestamp_new, '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['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 locale_test found'); - $this->assertEqual($result['custom_module_one']['de']->type, 'local', 'Translation of custom_module_one found'); - } -} diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php new file mode 100644 index 0000000..5137a64 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php @@ -0,0 +1,635 @@ + 'Update Interface translations', + 'description' => 'Tests for updating the interface translations of projects.', + 'group' => 'Locale', + ); + } + + /** + * Setup the test environment. + * + * We use German as default test language. Due to hardcoded configurations in + * the locale_test module, the language can not be chosen randomly. + */ + function setUp() { + parent::setUp(); + module_load_include('compare.inc', 'locale'); + module_load_include('fetch.inc', 'locale'); + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'translate interface')); + $this->drupalLogin($admin_user); + $this->drupalPost('admin/config/regional/language/add', array('predefined_langcode' => 'de'), t('Add language')); + + // Setup timestamps to identify old and new translation sources. + $this->timestamp_old = REQUEST_TIME - 300; + $this->timestamp_medium = REQUEST_TIME - 200; + $this->timestamp_new = REQUEST_TIME - 100; + $this->timestamp_now = REQUEST_TIME; + } + + /** + * 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; + file_prepare_directory($path, FILE_CREATE_DIRECTORY); + variable_set('locale_translate_file_directory', $path); + } + + /** + * Creates a translation file and tests 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 integer $timestamp + * Timestamp to set the file to. Defaults to current time. + * @param array $translations + * Array of source/target value translation strings. Only singular strings + * are supported, no plurals. + */ + private function makePoFile($path, $filename, $timestamp = NULL, $translations = array()) { + $timestamp = $timestamp ? $timestamp : REQUEST_TIME; + $path = 'public://' . $path; + $text = ''; + $po_header = << 1);\\n" + +EOF; + + // Convert array of translations to Gettext source an translation strings. + if ($translations) { + foreach ($translations as $source => $target) { + $text .= 'msgid "'. $source . '"' . "\n"; + $text .= 'msgstr "'. $target . '"' . "\n"; + } + } + + file_prepare_directory($path, FILE_CREATE_DIRECTORY); + $file = entity_create('file', array( + 'uid' => 1, + 'filename' => $filename, + 'uri' => $path . '/' . $filename, + 'filemime' => 'text/x-gettext-translation', + 'timestamp' => $timestamp, + 'status' => FILE_STATUS_PERMANENT, + )); + file_put_contents($file->uri, $po_header . $text); + touch(drupal_realpath($file->uri), $timestamp); + $file->save(); + } + + /** + * Setup the environment containting local and remote translation files. + * + * Update test require 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 files with a po extension to be accessed + * (denied in .htaccess) the translation files get a _po extension. Another + * directory is designated for local translation files. + */ + private function setTranslationFiles() { + $config = config('locale.settings'); + + // A flag is set to let the locale_test module replace the project data with + // a set of test projects which match the below project files. + state()->set('locale_translation_test_projects', TRUE); + + // Set up the environment. + $public_path = variable_get('file_public_path', conf_path() . '/files'); + $this->setTranslationsDirectory($public_path . '/local'); + $config->set('translation.default_filename', '%project-%version.%language._po')->save(); + + // Setting up sets of translations for the translation files. + $translations_one = array('January' => 'Januar_1', 'February' => 'Februar_1', 'March' => 'Marz_1'); + $translations_two = array( 'February' => 'Februar_2', 'March' => 'Marz_2', 'April' => 'April_2'); + $translations_three = array('April' => 'April_3', 'May' => 'Mai_3', 'June' => 'Juni_3'); + + // Add a number of files to the local file system to serve as remote + // translation server and match the project definitions set in + // locale_test_locale_translation_projects_alter(). + $this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de._po', $this->timestamp_new, $translations_one); + $this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestamp_old, $translations_two); + $this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de._po', $this->timestamp_old, $translations_three); + + // Add a number of files to the local file system to serve as local + // translation files and match the project definitions set in + // locale_test_locale_translation_projects_alter(). + $this->makePoFile('local', 'contrib_module_one-8.x-1.1.de._po', $this->timestamp_old, $translations_one); + $this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestamp_new, $translations_two); + $this->makePoFile('local', 'contrib_module_three-8.x-1.0.de._po', $this->timestamp_old, $translations_three); + $this->makePoFile('local', 'custom_module_one.de.po', $this->timestamp_new); + } + + /** + * Setup existing translations in the database and setup the status of existing translations. + */ + private function setCurrentTranslations() { + // Add non customized translations to the database. + $langcode = 'de'; + $context = ''; + $non_customized_translations = array( + 'March' => 'Marz', + 'June' => 'Juni', + ); + $customized = 0; + foreach ($non_customized_translations as $source => $translation) { + $string = locale_storage()->createString(array('source' => $source, 'context' => $context)) + ->save(); + $target = locale_storage()->createTranslation(array( + 'lid' => $string->getId(), + 'language' => $langcode, + 'translation' => $translation, + 'customized' => $customized, + ))->save(); + } + + // Add non customized translations to the database. + $customized_translations = array( + 'January' => 'Januar_customized', + 'February' => 'Februar_customized', + 'May' => 'Mai_customized', + ); + $customized = 1; + foreach ($customized_translations as $source => $translation) { + $string = locale_storage()->createString(array('source' => $source, 'context' => $context)) + ->save(); + $target = locale_storage()->createTranslation(array( + 'lid' => $string->getId(), + 'language' => $langcode, + 'translation' => $translation, + 'customized' => $customized, + ))->save(); + } + + // Add a state of current translations in locale_files. + $default = array( + 'langcode' => $langcode, + 'uri' => '', + 'timestamp' => $this->timestamp_medium, + 'last_checked' => $this->timestamp_medium, + ); + $data[] = array( + 'project' => 'contrib_module_one', + 'filename' => 'contrib_module_one-8.x-1.1.de._po', + 'version' => '8.x-1.1', + ); + $data[] = array( + 'project' => 'contrib_module_two', + 'filename' => 'contrib_module_two-8.x-2.0-beta4.de._po', + 'version' => '8.x-2.0-beta4', + ); + $data[] = array( + 'project' => 'contrib_module_three', + 'filename' => 'contrib_module_three-8.x-1.0.de._po', + 'version' => '8.x-1.0', + ); + $data[] = array( + 'project' => 'custom_module_one', + 'filename' => 'custom_module_one.de.po', + 'version' => '', + ); + foreach ($data as $file) { + $file = (object) array_merge($default, $file); + drupal_write_record('locale_file', $file); + } + } + + /** + * Test for translation status storage and translation status comparison. + */ + function testUpdateCompare() { + // Create and login user. + $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages')); + $this->drupalLogin($admin_user); + + module_load_include('compare.inc', 'locale'); + + // Check if hidden modules are not included. + $projects = locale_translation_project_list(); + $this->assertFalse(isset($projects['locale_test']), 'Hidden module not found'); + + // Make the test modules look like a normal custom module. i.e. make the + // modules not hidden. locale_test_system_info_alter() modifies the project + // info of the locale_test and locale_test_disabled modules. + state()->set('locale_translation_test_system_info_alter', TRUE); + + // Reset static system list caches to reflect info changes. + drupal_static_reset('locale_translation_project_list'); + system_list_reset(); + + // Check if interface translation data is collected from hook_info. + $projects = locale_translation_project_list(); + $this->assertEqual($projects['locale_test']['info']['interface translation server pattern'], 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.'); + $this->assertEqual($projects['locale_test']['name'] , 'locale_test', format_string('%key found in project info.', array('%key' => 'interface translation project'))); + + // Get the locale settings. + $config = config('locale.settings'); + + // Check if disabled modules are detected. + $config->set('translation.check_disabled_modules', TRUE)->save(); + drupal_static_reset('locale_translation_project_list'); + $projects = locale_translation_project_list(); + $this->assertTrue(isset($projects['locale_test_disabled']), 'Disabled module found'); + + // Check the fully processed list of project data of both enabled and + // disabled modules. + $config->set('translation.check_disabled_modules', TRUE)->save(); + drupal_static_reset('locale_translation_project_list'); + $projects = locale_translation_get_projects(); + $this->assertEqual($projects['drupal']->name, 'drupal', 'Core project found'); + $this->assertEqual($projects['locale_test']->server_pattern, 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.'); + $this->assertEqual($projects['locale_test_disabled']->status, '0', 'Disabled module found'); + $config->delete('translation.check_disabled_modules'); + + // Return the locale test modules back to their hidden state. + state()->delete('locale_translation_test_system_info_alter'); + } + + /** + * Checks if local or remote translation sources are detected. + * + * 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 checks whether + * the most recent files are selected in the different check scenarios: check + * for local files only, check for remote files only, check for both local and + * remote files. + */ + function testUpdateCheckStatus() { + $config = config('locale.settings'); + // A flag is set to let the locale_test module replace the project data with + // a set of test projects. + state()->set('locale_translation_test_projects', TRUE); + + // Create local and remote translations files. + $this->setTranslationFiles(); + $config->set('translation.default_filename', '%project-%version.%language._po')->save(); + + // Get status of translation sources at local file system. + $config->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)->save(); + $this->drupalGet('admin/reports/translations/check'); + $result = state()->get('locale_translation_status'); + $this->assertEqual($result['contrib_module_one']['de']->type, 'local', 'Translation of contrib_module_one found'); + $this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->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, $this->timestamp_new, 'Translation timestamp 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.use_source', LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL)->save(); + $this->drupalGet('admin/reports/translations/check'); + $result = state()->get('locale_translation_status'); + $this->assertEqual($result['contrib_module_one']['de']->type, 'remote', 'Translation of contrib_module_one found'); + $this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestamp_new, '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, $this->timestamp_new, 'Translation timestamp found'); + $this->assertEqual($result['contrib_module_three']['de']->type, 'local', 'Translation of contrib_module_three found'); + $this->assertEqual($result['contrib_module_three']['de']->timestamp, $this->timestamp_old, 'Translation timestamp 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'); + } + + /** + * Test translation import from remote sources + * + * Test conditions: + * - Source: remote + * - Overwrite: all + * - Translation directory: available + * Test results after download: + * - Source in status array: contrib_module_one = current source, contrib_module_two = current source, contrib_module_three = current source. + * - Timestamp in import_files table: contrib_module_one = now, contrib_module_two = now, contrib_module_three = medium. + * - Translation in table locales_target: non-customized translations are overwritten, customized translations are overwritten. + */ + function testUpdateImportSourceRemote() { + $config = config('locale.settings'); + + // Build the test environment. + $this->setTranslationFiles(); + $this-> setCurrentTranslations(); + $config->set('translation.default_filename', '%project-%version.%language._po'); + + // Set the test conditions. + $edit = array( + 'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL, + 'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL, + ); + $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); + + // Execute translation update. + $this->drupalGet('admin/reports/translations/check'); + $this->drupalPost('admin/reports/translations', array(), t('Update')); + + // @todo + $status = state()->get('locale_translation_status'); + $this->assertEqual($status['contrib_module_one']['de']->type, 'current', 'Translation of contrib_module_one found'); + $this->assertEqual($status['contrib_module_two']['de']->type, 'current', 'Translation of contrib_module_two found'); + $this->assertEqual($status['contrib_module_three']['de']->type, 'current', 'Translation of contrib_module_three found'); + + // @todo + drupal_static_reset('locale_translation_get_file_history'); + $history = locale_translation_get_file_history(); + $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_now, 'Translation of contrib_module_one is imported'); + $this->assertTrue($history['contrib_module_one']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_one is updated'); + $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation of contrib_module_two is imported'); + $this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_two is updated'); + $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported'); + $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated'); + + // @todo + $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_1', 'Translation of January'); + $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February'); + $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March'); + $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April'); + $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May'); + $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June'); + $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday'); + } + + /** + * Test translation import from local sources + * + * @todo Finish documentation. + * Starting point: + * Time stamp of translations: + * contrib_module_one + * remote: new + * local: old + * current: medium + * contrib_module_two + * remote: old + * local: new + * current: medium + * contrib_module_three + * remote: old + * local: old + * current: medium + * Translations per project + * Customized translations + * + * Test conditions: + * - Source: local + * - Overwrite: all + * - Translation directory: available + * Test results: + * - In status array: contrib_module_one = local source, contrib_module_two = local source, contrib_module_three = current (or no) source. + * - In import_files table: contrib_module_one = medium, contrib_module_two = now, contrib_module_three = medium. + * - In table locales_target: non-customized translations are overwritten, customized translations are overwritten. + */ + function testUpdateImportSourceLocal() { + $config = config('locale.settings'); + + // Build the test environment. + $this->setTranslationFiles(); + $this-> setCurrentTranslations(); + $config->set('translation.default_filename', '%project-%version.%language._po'); + + // Set the test conditions. + $edit = array( + 'use_source' => LOCALE_TRANSLATION_USE_SOURCE_LOCAL, + 'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL, + ); + $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); + + // Execute translation update. + $this->drupalGet('admin/reports/translations/check'); + $this->drupalPost('admin/reports/translations', array(), t('Update')); + + // @todo + $status = state()->get('locale_translation_status'); + $this->assertEqual($status['contrib_module_one']['de']->type, 'current', 'Translation of contrib_module_one found'); + $this->assertEqual($status['contrib_module_two']['de']->type, 'current', 'Translation of contrib_module_two found'); + $this->assertEqual($status['contrib_module_three']['de']->type, 'current', 'Translation of contrib_module_three found'); + + // @todo + drupal_static_reset('locale_translation_get_file_history'); + $history = locale_translation_get_file_history(); + $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_medium, 'Translation of contrib_module_one is imported'); + $this->assertEqual($history['contrib_module_one']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_one is updated'); + $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation of contrib_module_two is imported'); + $this->assertTrue($history['contrib_module_two']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_two is updated'); + $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported'); + $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated'); + + // @todo + $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January'); + $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_2', 'Translation of February'); + $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March'); + $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April'); + $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May'); + $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June'); + $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday'); + } + + /** + * Test translation import without a translations directory. + * + * Test conditions: + * - Source: remote + * - Overwrite: all + * - Translation directory: not available + * Test results: + * - Source in status array: contrib_module_one = remote source, contrib_module_two = remote source, contrib_module_three = current (or no) source. + * - Timestamp in import_files table: contrib_module_one = now, contrib_module_two = now, contrib_module_three = medium. + */ + function testUpdateImportWithoutDirectory() { + $config = config('locale.settings'); + + // Build the test environment. + $this->setTranslationFiles(); + $this-> setCurrentTranslations(); + $config->set('translation.default_filename', '%project-%version.%language._po'); + + // Set the test conditions. + $this->setTranslationsDirectory(''); + $edit = array( + 'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL, + 'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL, + ); + $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); + + // Execute translation update. + $this->drupalGet('admin/reports/translations/check'); + $this->drupalPost('admin/reports/translations', array(), t('Update')); + + // @todo + $status = state()->get('locale_translation_status'); + $this->assertEqual($status['contrib_module_one']['de']->type, 'current', 'Translation of contrib_module_one found'); + $this->assertEqual($status['contrib_module_two']['de']->type, 'current', 'Translation of contrib_module_two found'); + $this->assertEqual($status['contrib_module_three']['de']->type, 'current', 'Translation of contrib_module_three found'); + + // @todo + drupal_static_reset('locale_translation_get_file_history'); + $history = locale_translation_get_file_history(); + $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_now, 'Translation of contrib_module_one is imported'); + $this->assertTrue($history['contrib_module_one']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_one is updated'); + $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_two is imported'); + $this->assertEqual($history['contrib_module_two']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_two is updated'); + $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported'); + $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated'); + + // @todo + $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_1', 'Translation of January'); + $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_1', 'Translation of February'); + $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_1', 'Translation of March'); + $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May'); + $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June'); + $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday'); + } + + /** + * Test translation import with and only overwrite non-customized translations. + * + * Test conditions: + * - Source: remote + * - Overwrite: non-customized + * - Translation directory: available + * Test results: + * - In table locales_target: non-customized translations are overwritten, customized translations are not overwritten + */ + function testUpdateImportModeNonCustomized() { + $config = config('locale.settings'); + + // Build the test environment. + $this->setTranslationFiles(); + $this-> setCurrentTranslations(); + $config->set('translation.default_filename', '%project-%version.%language._po'); + + // Set the test conditions. + $edit = array( + 'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL, + 'overwrite' => LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED, + ); + $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); + + // Execute translation update. + $this->drupalGet('admin/reports/translations/check'); + $this->drupalPost('admin/reports/translations', array(), t('Update')); + + // @todo + $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January'); + $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_customized', 'Translation of February'); + $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_2', 'Translation of March'); + $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April'); + $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May'); + $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June'); + $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday'); + } + + /** + * Test translation import with and don't overwrite any translation. + * + * Test conditions: + * - Source: remote + * - Overwrite: none + * - Translation directory: available + * Test results: + * - In table locales_target: non-customized translations are not overwritten, customized translations are not overwritten + */ + function testUpdateImportModeNone() { + $config = config('locale.settings'); + + // Build the test environment. + $this->setTranslationFiles(); + $this-> setCurrentTranslations(); + $config->set('translation.default_filename', '%project-%version.%language._po'); + + // Set the test conditions. + $edit = array( + 'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL, + 'overwrite' => LOCALE_TRANSLATION_OVERWRITE_NONE, + ); + $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); + + // Execute translation update. + $this->drupalGet('admin/reports/translations/check'); + $this->drupalPost('admin/reports/translations', array(), t('Update')); + + // @todo + $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_customized', 'Translation of January'); + $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_customized', 'Translation of February'); + $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz', 'Translation of March'); + $this->assertEqual(t('April', array(), array('langcode' => 'de')), 'April_2', 'Translation of April'); + $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May'); + $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June'); + $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday'); + } +} diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc index c4b6acd..7fda10f 100644 --- a/core/modules/locale/locale.batch.inc +++ b/core/modules/locale/locale.batch.inc @@ -130,7 +130,6 @@ function locale_translation_batch_status_compare(&$context) { } } - $results[$project][$langcode] = $source; } } @@ -148,7 +147,7 @@ function locale_translation_batch_status_compare(&$context) { */ function locale_translation_batch_status_finished($success, $results) { $t = get_t(); - if($success) { + if ($success) { if ($results['sources']) { drupal_set_message(format_plural( count($results['sources']), @@ -258,7 +257,7 @@ function locale_translation_batch_fetch_import($source, $options, &$context) { // 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; + $source_result->files['import']->last_checked = REQUEST_TIME; file_unmanaged_delete($file->uri); } if ($import_type == 'local') { @@ -266,7 +265,7 @@ function locale_translation_batch_fetch_import($source, $options, &$context) { $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; + $source_result->files['import']->last_checked = REQUEST_TIME; } } diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 6168845..bf10dc5 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -507,7 +507,7 @@ function locale_translate_batch_import($file, $options, &$context) { // Store the file data for processing by the next batch operation. $file->timestamp = filemtime($file->uri); $context['results']['files'][$file->uri] = $file; - $context['results']['languages'][$filepath] = $file->langcode; + $context['results']['languages'][$file->uri] = $file->langcode; } // Add the values from the report to the stats for this file. if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) { @@ -546,7 +546,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; + $file->last_checked = REQUEST_TIME; locale_translation_update_file_history($file); } } diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index 13604c6..b788cca 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -405,6 +405,7 @@ function locale_translation_batch_status_build($sources) { */ function locale_translation_check_projects_local($projects, $langcodes = NULL) { $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $history = locale_translation_get_file_history(); $results = array(); // For each project and each language we check if a local po file is @@ -417,12 +418,33 @@ function locale_translation_check_projects_local($projects, $langcodes = NULL) { $source->type = 'local'; $source->timestamp = $source->files['local']->timestamp; } + + // 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[$name][$langcode] = $source; } } - - state()->set('locale_translation_status', $results); - state()->set('locale_translation_status_last_update', REQUEST_TIME); + locale_translation_status_save($results); } /** @@ -434,9 +456,9 @@ function locale_translation_check_projects_local($projects, $langcodes = NULL) { * * 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(). + * %project-%version.%language.po. 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 source object. diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index cfc4931..8578985 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -94,18 +94,25 @@ const LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN = 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po'; /** - * 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. + * The number of seconds that the translations status entry should be considered. */ -const LOCALE_TRANSLATION_DEFAULT_FILENAME = '%project-%version.%language.po'; +const LOCALE_TRANSLATION_STATUS_TTL = 600; /** - * The number of seconds that the translations status entry should be considered. + * @todo */ -const LOCALE_TRANSLATION_STATUS_TTL = 600; +const LOCALE_TRANSLATION_OVERWRITE_ALL = 'all'; + +/** + * @todo + */ +const LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED = 'non_customized'; + +/** + * @todo + */ +const LOCALE_TRANSLATION_OVERWRITE_NONE = 'none'; + /** * Implements hook_help(). @@ -181,6 +188,15 @@ function locale_menu() { 'type' => MENU_LOCAL_TASK, 'file' => 'locale.bulk.inc', ); + $items['admin/config/regional/translate/settings'] = array( + 'title' => 'Settings', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('locale_translate_settings'), + 'access arguments' => array('translate interface'), + 'weight' => 40, + 'type' => MENU_LOCAL_TASK, + 'file' => 'locale.pages.inc', + ); $items['admin/reports/translations'] = array( 'title' => 'Available translation updates', 'description' => 'Get a status report about available interface translations for your installed modules and themes.', @@ -704,7 +720,7 @@ function locale_form_system_file_system_settings_alter(&$form, $form_state) { '#title' => t('Interface translations directory'), '#default_value' => variable_get('locale_translate_file_directory', conf_path() . '/files/translations'), '#maxlength' => 255, - '#description' => t('A local file system path where interface translation files are looked for. This directory must exist.'), + '#description' => t('A local file system path where interface translation files will be stored.'), '#after_build' => array('system_check_directory'), '#weight' => 10, ); diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 2386fdb..6cec6fa 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -467,6 +467,118 @@ function locale_translation_manual_status() { } /** + * Page callback: Configuration for interface translation. + * + * @see locale_menu() + */ +function locale_translate_settings($form, &$form_state) { + $config = config('locale.settings'); + + $form['update_interval_days'] = array( + '#type' => 'radios', + '#title' => t('Check for updates'), + '#default_value' => $config->get('translation.update_interval_days'), + '#options' => array( + '0' => t('Never (manually)'), + '1' => t('Daily'), + '7' => t('Weekly'), + ), + '#description' => t('Select how frequently you want to check for new interface translations for your currently installed modules and themes.'), + ); + + $form['check_disabled_modules'] = array( + '#type' => 'checkbox', + '#title' => t('Check for updates of disabled modules and themes'), + '#default_value' => $config->get('translation.check_disabled_modules'), + ); + + if ($directory = variable_get('locale_translate_file_directory', conf_path() . '/files/translations')) { + $description = t('Translation files are stored locally in the %path directory. You can change this directory on the File system configuration page.', array('%path' => $directory, '@url' => url('admin/config/media/file-system'))); + } + else { + $description = t('Translation files will not be stored locally. Change the Interface translation directory on the File system configuration page.', array('@url' => url('admin/config/media/file-system'))); + } + $form['#translation_directory'] = $directory; + $form['use_source'] = array( + '#type' => 'radios', + '#title' => t('Translation source'), + '#default_value' => $config->get('translation.use_source'), + '#options' => array( + LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL => t('Remote and local'), + LOCALE_TRANSLATION_USE_SOURCE_LOCAL => t('Local only'), + ), + '#description' => t('The source of translation files for automatic interface translation.') . ' ' . $description, + ); + + if ($config->get('translation.overwrite_not_customized') == FALSE) { + $default = LOCALE_TRANSLATION_OVERWRITE_NONE; + } + elseif ($config->get('translation.overwrite_customized') == TRUE) { + $default = LOCALE_TRANSLATION_OVERWRITE_ALL; + } + else { + $default = LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED; + } + $form['overwrite'] = array( + '#type' => 'radios', + '#title' => t('Import behaviour'), + '#default_value' => $default, + '#options' => array( + LOCALE_TRANSLATION_OVERWRITE_NONE => t("Don't overwrite existing translations."), + LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED => t('Only overwrite imported translations, customized translations are kept.'), + LOCALE_TRANSLATION_OVERWRITE_ALL => t('Overwrite existing translations.'), + ), + '#description' => t('How to treat existing translations when automatically updating the interface translations.'), + ); + + return system_config_form($form, $form_state); +} + +/** + * @todo + */ +function locale_translate_settings_validate($form, &$form_state) { + if (empty($form['#translation_directory']) && $form_state['values']['use_source'] == LOCALE_TRANSLATION_USE_SOURCE_LOCAL) { + form_set_error('use_source', t('You have selected local translation source, but no Interface translation directory was configured.', array('@url' => url('admin/config/media/file-system')))); + } +} + +/** + * @todo + */ +function locale_translate_settings_submit($form, &$form_state) { + $values = $form_state['values']; + + $config = config('locale.settings'); + $config->set('translation.update_interval_days', $values['update_interval_days'])->save(); + $config->set('translation.use_source', $values['use_source'])->save(); + + switch ($values['overwrite']) { + case LOCALE_TRANSLATION_OVERWRITE_ALL: + $config->set('translation.overwrite_customized', TRUE)->save(); + $config->set('translation.overwrite_not_customized', TRUE)->save(); + break; + case LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED: + $config->set('translation.overwrite_customized', FALSE)->save(); + $config->set('translation.overwrite_not_customized', TRUE)->save(); + break; + case LOCALE_TRANSLATION_OVERWRITE_NONE: + $config->set('translation.overwrite_customized', FALSE)->save(); + $config->set('translation.overwrite_not_customized', FALSE)->save(); + break; + } + + $config->set('translation.check_disabled_modules', $values['check_disabled_modules'])->save(); + + // Invalidate the cached translation status when the configuration setting of + // 'use_source' and 'check_disabled_modules' change. + if ($form['use_source']['#default_value'] != $form_state['values']['use_source'] || + $form['check_disabled_modules']['#default_value'] != $form_state['values']['check_disabled_modules']) { + locale_translation_clear_status(); + } +} + +/** * Page callback: Display the current translation status. * * @see locale_menu() 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 03e455d..e084915 100644 --- a/core/modules/locale/tests/modules/locale_test/locale_test.module +++ b/core/modules/locale/tests/modules/locale_test/locale_test.module @@ -52,7 +52,7 @@ function locale_test_locale_translation_projects_alter(&$projects) { 'name' => 'drupal', 'info' => array ( 'name' => 'Drupal', - 'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language.txt', + 'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po', 'package' => 'Core', 'version' => '8.0', 'project' => 'drupal', @@ -67,7 +67,7 @@ function locale_test_locale_translation_projects_alter(&$projects) { 'name' => 'contrib_module_one', 'info' => array ( 'name' => 'Contributed module one', - 'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language.txt', + 'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po', 'package' => 'Other', 'version' => '8.x-1.1', 'project' => 'contrib_module_one', @@ -82,7 +82,7 @@ function locale_test_locale_translation_projects_alter(&$projects) { 'name' => 'contrib_module_two', 'info' => array ( 'name' => 'Contributed module two', - 'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language.txt', + 'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po', 'package' => 'Other', 'version' => '8.x-2.0-beta4', 'project' => 'contrib_module_two', @@ -97,7 +97,7 @@ function locale_test_locale_translation_projects_alter(&$projects) { 'name' => 'contrib_module_three', 'info' => array ( 'name' => 'Contributed module three', - 'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language.txt', + 'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po', 'package' => 'Other', 'version' => '8.x-1.0', 'project' => 'contrib_module_three',