diff --git a/core/modules/locale/lib/Drupal/locale/Gettext.php b/core/modules/locale/lib/Drupal/locale/Gettext.php index 43432e4..b21023a 100644 --- a/core/modules/locale/lib/Drupal/locale/Gettext.php +++ b/core/modules/locale/lib/Drupal/locale/Gettext.php @@ -53,9 +53,8 @@ static function filesToArray($langcode, array $files) { * * @param stdClass $file * File object with an URI property pointing at the file's path. - * @todo: Describe Required parts of the file object. - * - 'langcode': The language code. - * + * - "langcode": The language the language the strings will be added to. + * - "uri": File URI. * @param array $options * An array with options that can have the following elements: * - 'overwrite_options': Overwrite options array as defined in diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php index 337bd90..eb02890 100644 --- a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php @@ -160,14 +160,14 @@ function setHeader(PoHeader $header) { // Check for options. $options = $this->getOptions(); if (empty($options)) { - throw new \Exception("Options should be set before assigning a PoHeader."); + throw new \Exception('Options should be set before assigning a PoHeader.'); } $overwrite_options = $options['overwrite_options']; // Check for langcode. $langcode = $this->_langcode; if (empty($langcode)) { - throw new \Exception("Langcode should be set before assigning a PoHeader."); + throw new \Exception('Langcode should be set before assigning a PoHeader.'); } if (array_sum($overwrite_options) || empty($locale_plurals[$langcode]['plurals'])) { diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php deleted file mode 100644 index 126fbbc..0000000 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleFileImportStatus.php +++ /dev/null @@ -1,202 +0,0 @@ - 'Translation file import status', - 'description' => 'Tests the status of imported translation files.', - 'group' => 'Locale', - ); - } - - function setUp() { - parent::setUp(); - // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages')); - $this->drupalLogin($admin_user); - - // Copy test po files to the translations directory. - file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.de.po', 'translations://', FILE_EXISTS_REPLACE); - file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.xx.po', 'translations://', FILE_EXISTS_REPLACE); - } - - /** - * Add a language. - * - * @param $langcode - * The language of the langcode to add. - */ - function addLanguage($langcode) { - $edit = array('predefined_langcode' => $langcode); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); - drupal_static_reset('language_list'); - $this->assertTrue(language_load($langcode), t('Language %langcode added.', array('%langcode' => $langcode))); - } - - /** - * Get translations for an array of strings. - * - * @param $strings - * An array of strings to translate. - * @param $langcode - * The language code of the language to translate to. - */ - function checkTranslations($strings, $langcode) { - foreach ($strings as $source => $translation) { - $db_translation = db_query('SELECT translation FROM {locales_target} lt INNER JOIN {locales_source} ls ON ls.lid = lt.lid WHERE ls.source = :source AND lt.language = :langcode', array(':source' => $source, ':langcode' => $langcode))->fetchField(); - $this->assertEqual((string) $translation, (string) $db_translation); - } - } - - /** - * Import a single interface translation file. - * - * @param $langcode - * Langcode of the po file and language to import. - * @param int $timestamp_difference - * (optional) Timestamp offset, used to mock older or newer files. - * - * @return stdClass - * A file object of type stdClass. - */ - function mockImportedPoFile($langcode, $timestamp_difference = 0) { - $testfile_uri = 'translations://test.' . $langcode . '.po'; - - $file = locale_translate_file_create($testfile_uri); - $file->original_timestamp = $file->timestamp; - $file->timestamp = $file->timestamp + $timestamp_difference; - $file = locale_translate_file_attach_properties($file, array('langcode' => $langcode)); - - // Fill the {locale_file} with a custom timestamp. - if ($timestamp_difference != 0) { - locale_translation_update_file_history($file); - } - - $count = db_query('SELECT COUNT(*) FROM {locale_file} WHERE langcode = :langcode', array(':langcode' => $langcode))->fetchField(); - $this->assertEqual(1, $count, format_plural($count, '@count file registered in {locale_file}.', '@count files registered in {locale_file}.')); - - $result = db_query('SELECT langcode, uri FROM {locale_file}')->fetchAssoc(); - $this->assertEqual($result['uri'], $testfile_uri, t('%uri is in {locale_file}.', array('%uri' => $result['uri']))); - $this->assertEqual($result['langcode'], $langcode, t('Langcode is %langcode.', array('%langcode' => $langcode))); - return $file; - } - - /** - * Test the basic bulk import functionality. - */ - function testBulkImport() { - $langcode = 'de'; - - // Translations should not exist. - $strings = array( - 'Monday' => '', - 'Tuesday' => '', - ); - $this->checkTranslations($strings, $langcode); - - // Add language. - $this->addLanguage($langcode); - - // The file was imported, translations should exist. - $strings = array( - 'Monday' => 'Montag', - 'Tuesday' => 'Dienstag', - ); - $this->checkTranslations($strings, $langcode); - } - - /** - * Update a pre-existing file. - */ - function testBulkImportUpdateExisting() { - $langcode = 'de'; - - // Translations should not exist. - $strings = array( - 'Monday' => '', - 'Tuesday' => '', - ); - $this->checkTranslations($strings, $langcode); - - // Fill the {locale_file} table with an older file. - $file = $this->mockImportedPoFile($langcode, -1); - - // Add language. - $this->addLanguage($langcode); - - // The file was imported, translations should exist. - $strings = array( - 'Monday' => 'Montag', - 'Tuesday' => 'Dienstag', - ); - $this->checkTranslations($strings, $langcode); - - $timestamp = db_query('SELECT timestamp FROM {locale_file} WHERE uri = :uri', array(':uri' => $file->uri))->fetchField(); - // Ensure that $timestamp is now greater than the manipulated timestamp. - $this->assertTrue($timestamp > $file->timestamp, 'Timestamp on locale_file updated.'); - } - - /** - * Don't update a pre-existing file. - */ - function testBulkImportNotUpdateExisting() { - $langcode = 'de'; - - // Translations should not exist. - $strings = array( - 'Monday' => '', - 'Tuesday' => '', - ); - $this->checkTranslations($strings, $langcode); - - // Fill the {locale_file} table with a newer file. - $file = $this->mockImportedPoFile($langcode, 1); - - // Add language. - $this->addLanguage($langcode); - - // The file was not imported, the translation should not exist. - $strings = array( - 'Monday' => '', - 'Tuesday' => '', - ); - $this->checkTranslations($strings, $langcode); - - $timestamp = db_query('SELECT timestamp FROM {locale_file} WHERE uri = :uri', array(':uri' => $file->uri))->fetchField(); - $this->assertEqual($timestamp, $file->timestamp); - } - - /** - * Delete translation files after deleting a language. - */ - function testDeleteLanguage() { - $langcode = 'de'; - $this->addLanguage($langcode); - $file_uri = 'translations://po_' . $this->randomName() . '.' . $langcode . '.po'; - file_put_contents(drupal_realpath($file_uri), $this->randomName()); - $this->assertTrue(is_file($file_uri), 'Translation file is created.'); - language_delete($langcode); - $this->assertTrue($file_uri); - $this->assertFalse(is_file($file_uri), 'Translation file deleted after deleting language'); - } -} diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php index 4dca9e7..fd3667c 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleImportFunctionalTest.php @@ -58,7 +58,7 @@ function testStandalonePoFile() { $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), t('The language has been automatically created.')); // The import should have created 8 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); // This import should have saved plural forms to have 2 variants. $locale_plurals = variable_get('locale_translation_plurals', array()); @@ -74,8 +74,9 @@ function testStandalonePoFile() { )); // The import should have created 1 string and rejected 2. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); - $skip_message = format_plural(2, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => url('admin/reports/dblog'))); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + + $skip_message = format_plural(2, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => url('admin/reports/dblog'))); $this->assertRaw($skip_message, t('Unsafe strings were skipped.')); // Try importing a zero byte sized .po file. @@ -84,7 +85,7 @@ function testStandalonePoFile() { )); // The import should have created 0 string and rejected 0. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 0)), 'The empty translation file was successfully imported.'); + $this->assertRaw(t('One translation file could not be imported. See the log for details.', array('@url' => url('admin/reports/dblog'))), 'The empty translation file was successfully imported.'); // Try importing a .po file which doesn't exist. $name = $this->randomName(16); @@ -103,7 +104,7 @@ function testStandalonePoFile() { )); // The import should have created 1 string. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); // Ensure string wasn't overwritten. $search = array( 'string' => 'Montag', @@ -125,7 +126,7 @@ function testStandalonePoFile() { )); // The import should have updated 2 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.')); // Ensure string was overwritten. $search = array( 'string' => 'Montag', @@ -145,7 +146,7 @@ function testStandalonePoFile() { )); // The import should have created 6 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 6, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.')); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 6, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.')); // The database should now contain 6 customized strings (two imported // strings are not translated). @@ -161,7 +162,7 @@ function testStandalonePoFile() { )); // The import should have created 1 string. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.')); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.')); // Ensure string wasn't overwritten. $search = array( 'string' => 'januari', @@ -180,7 +181,7 @@ function testStandalonePoFile() { )); // The import should have updated 2 strings. - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The customized translation file was successfully imported.')); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The customized translation file was successfully imported.')); // Ensure string was overwritten. $search = array( 'string' => 'januari', @@ -193,60 +194,6 @@ function testStandalonePoFile() { } /** - * Test automatic import of a module's translation files. - */ - function testAutomaticModuleTranslationImportLanguageEnable() { - // Code for the language - manually set to match the test translation file. - $langcode = 'xx'; - // The English name for the language. - $name = $this->randomName(16); - - // Create a custom language. - $edit = array( - 'predefined_langcode' => 'custom', - 'langcode' => $langcode, - 'name' => $name, - 'direction' => '0', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); - - // Ensure the translation file was automatically imported when language was - // added. - $this->assertText(t('One translation file imported.'), t('Language file automatically imported.')); - $this->assertText(t('A translation string was skipped because of disallowed or malformed HTML'), t('Language file automatically imported.')); - - // Ensure strings were successfully imported. - $search = array( - 'string' => 'lundi', - 'langcode' => $langcode, - 'translation' => 'translated', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertNoText(t('No strings available.'), t('String successfully imported.')); - - // Ensure multiline string was imported. - $search = array( - 'string' => 'Source string for multiline translation', - 'langcode' => $langcode, - 'translation' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText('Multiline translation string to make sure that import works with it.', t('String successfully imported.')); - - // Ensure 'Allowed HTML source string' was imported but the translation for - // 'Another allowed HTML source string' was not because it contains invalid - // HTML. - $search = array( - 'string' => 'HTML source string', - 'langcode' => $langcode, - 'translation' => 'all', - ); - $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); - $this->assertText('Allowed HTML source string', t('String successfully imported.')); - $this->assertNoText('Another allowed HTML source string', t('String with disallowed translation not imported.')); - } - - /** * Test msgctxt context support. */ function testLanguageContext() { @@ -270,7 +217,7 @@ function testEmptyMsgstr() { 'langcode' => $langcode, )); - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.')); $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', t('String imported and translated.')); // Try importing a .po file. @@ -278,7 +225,7 @@ function testEmptyMsgstr() { 'langcode' => $langcode, 'overwrite_options[not_customized]' => TRUE, )); - $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.')); + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.')); $str = "Operations"; $search = array( diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php index b38e0d0..07a1887 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php @@ -22,34 +22,27 @@ class LocaleUpdateTest extends WebTestBase { private $tranlations_directory; /** - * Timestamp for a translation file or existing translations. + * Timestamp for an old translation. * * @var integer */ private $timestamp_old; /** - * Timestamp for a translation file or existing translations. + * Timestamp for a medium aged translation. * * @var integer */ private $timestamp_medium; /** - * Timestamp for a translation file or existing translations. + * Timestamp for a new translation. * * @var integer */ private $timestamp_new; /** - * Timestamp for a translation file or existing translations. - * - * @var integer - */ - private $timestamp_current; - - /** * Modules to enable. * * @var array @@ -74,7 +67,7 @@ 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')); + $admin_user = $this->drupalCreateUser(array('administer modules', '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')); @@ -99,6 +92,19 @@ private function setTranslationsDirectory($path) { } /** + * Add a language. + * + * @param $langcode + * The language code of the language to add. + */ + function addLanguage($langcode) { + $edit = array('predefined_langcode' => $langcode); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + drupal_static_reset('language_list'); + $this->assertTrue(language_load($langcode), t('Language %langcode added.', array('%langcode' => $langcode))); + } + + /** * Creates a translation file and tests its timestamp. * * @param string $path @@ -126,7 +132,7 @@ private function makePoFile($path, $filename, $timestamp = NULL, $translations = EOF; - // Convert array of translations to Gettext source an translation strings. + // Convert array of translations to Gettext source and translation strings. if ($translations) { foreach ($translations as $source => $target) { $text .= 'msgid "'. $source . '"' . "\n"; @@ -151,20 +157,37 @@ private function makePoFile($path, $filename, $timestamp = NULL, $translations = /** * Setup the environment containting local and remote translation files. * - * Update test require a simulated environment for local and remote files. + * Update tests 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. + * + * The environment is set up with the following files. File creation times are + * set to create different variations in test conditions. + * contrib_module_one + * - remote file: timestamp new + * - local file: timestamp old + * contrib_module_two + * - remote file: timestamp old + * - local file: timestamp new + * contrib_module_three + * - remote file: timestamp old + * - local file: timestamp old + * custom_module_one + * - local file: timestamp new + * Time stamp of current translation set by setCurrentTranslations() is always + * timestamp medium. This makes it easy to predict which translation will be + * imported. */ 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); + state()->set('locale.test_projects_alter', TRUE); // Set up the environment. $public_path = variable_get('file_public_path', conf_path() . '/files'); @@ -193,7 +216,8 @@ private function setTranslationFiles() { } /** - * Setup existing translations in the database and setup the status of existing translations. + * Setup existing translations in the database and setup the status of + * existing translations. */ private function setCurrentTranslations() { // Add non customized translations to the database. @@ -215,7 +239,7 @@ private function setCurrentTranslations() { ))->save(); } - // Add non customized translations to the database. + // Add customized translations to the database. $customized_translations = array( 'January' => 'Januar_customized', 'February' => 'Februar_customized', @@ -267,54 +291,58 @@ private function setCurrentTranslations() { } /** - * Test for translation status storage and translation status comparison. + * Check the translation of an array of strings. + * + * @param string $source + * Translation source string + * @param string $translation + * Translation to check. Use empty string to check for a not existing + * translation. + * @param $langcode + * Language code of the language to translate to. */ - function testUpdateCompare() { - // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages')); - $this->drupalLogin($admin_user); + function assertTranslation($source, $translation, $langcode, $message = '') { + $db_translation = db_query('SELECT translation FROM {locales_target} lt INNER JOIN {locales_source} ls ON ls.lid = lt.lid WHERE ls.source = :source AND lt.language = :langcode', array(':source' => $source, ':langcode' => $langcode))->fetchField(); + $this->assertEqual((string) $translation, (string) $db_translation, $message ? $message : format_string('Correct translation of %source (%language)', array('%source' => $source, '%language' => $langcode))); + } + /** + * Check if a list of translatable projects gets build. + */ + function testUpdateProjects() { 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(); + state()->set('locale.test_system_info_alter', TRUE); // Check if interface translation data is collected from hook_info. $projects = locale_translation_project_list(); + $this->assertFalse(isset($projects['locale_test_disabled']), 'Hidden module not found'); $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. + /** + * Check if a list of translatable projects can include hidden projects. + */ + function testUpdateProjectsHidden() { + module_load_include('compare.inc', 'locale'); $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'); + // Make the test modules look like a normal custom module. + state()->set('locale.test_system_info_alter', TRUE); - // 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'); + // Set test condition: include disabled modules when building a project list. + $edit = array( + 'check_disabled_modules' => TRUE, + ); + $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); - // Return the locale test modules back to their hidden state. - state()->delete('locale_translation_test_system_info_alter'); + $projects = locale_translation_project_list(); + $this->assertTrue(isset($projects['locale_test_disabled']), 'Disabled module found'); + $this->assertTrue(isset($projects['locale_test']), 'Enabled module found'); } /** @@ -336,7 +364,7 @@ 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); + state()->set('locale.test_projects_alter', TRUE); // Create local and remote translations files. $this->setTranslationFiles(); @@ -351,7 +379,7 @@ function testUpdateCheckStatus() { // 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'); + $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'); @@ -365,9 +393,9 @@ function testUpdateCheckStatus() { ); $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); - // Get status of translation sources at both local and remote the locations. + // Get status of translation sources at both local and remote locations. $this->drupalGet('admin/reports/translations/check'); - $result = state()->get('locale_translation_status'); + $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'); @@ -379,24 +407,6 @@ function testUpdateCheckStatus() { } /** - * 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 - * - */ - - /** * Test translation import from remote sources. * * Test conditions: @@ -424,13 +434,13 @@ function testUpdateImportSourceRemote() { $this->drupalPost('admin/reports/translations', array(), t('Update')); // Check if the translation has been updated, using the status cache. - $status = state()->get('locale_translation_status'); + $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'); // Check the new translation status. - // The static cache needs to be flushed firt to get the most recent data + // The static cache needs to be flushed first to get the most recent data // from the database. The function was called earlier during this test. drupal_static_reset('locale_translation_get_file_history'); $history = locale_translation_get_file_history(); @@ -452,8 +462,7 @@ function testUpdateImportSourceRemote() { } /** - * Test translation import from local sources - * + * Test translation import from local sources. * * Test conditions: * - Source: local files only @@ -480,7 +489,7 @@ function testUpdateImportSourceLocal() { $this->drupalPost('admin/reports/translations', array(), t('Update')); // Check if the translation has been updated, using the status cache. - $status = state()->get('locale_translation_status'); + $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'); @@ -536,7 +545,7 @@ function testUpdateImportWithoutDirectory() { $this->drupalPost('admin/reports/translations', array(), t('Update')); // Check if the translation has been updated, using the status cache. - $status = state()->get('locale_translation_status'); + $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'); @@ -563,7 +572,8 @@ function testUpdateImportWithoutDirectory() { } /** - * Test translation import with and only overwrite non-customized translations. + * Test translation import with a translations directory and only overwrite + * non-customized translations. * * Test conditions: * - Source: remote and local files @@ -600,7 +610,8 @@ function testUpdateImportModeNonCustomized() { } /** - * Test translation import with and don't overwrite any translation. + * Test translation import with a translations directory and don't overwrite + * any translation. * * Test conditions: * - Source: remote and local files @@ -627,12 +638,153 @@ function testUpdateImportModeNone() { $this->drupalPost('admin/reports/translations', array(), t('Update')); // Check whether existing translations have (not) been overwritten. - $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'); + $this->assertTranslation('January', 'Januar_customized', 'de'); + $this->assertTranslation('February', 'Februar_customized', 'de'); + $this->assertTranslation('March', 'Marz', 'de'); + $this->assertTranslation('April', 'April_2', 'de'); + $this->assertTranslation('May', 'Mai_customized', 'de'); + $this->assertTranslation('June', 'Juni', 'de'); + $this->assertTranslation('Monday', 'Montag', 'de'); } + + /** + * Test automatic translation import when a module is enabled. + */ + function testEnableDisableModule() { + // Check is there is no translation yet. + $this->assertTranslation('Tuesday', '', 'de'); + + // Enable a module. + $edit = array( + 'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate', + ); + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + + // Check if translations have been imported. + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', + array('%number' => 7, '%update' => 0, '%delete' => 0)), 'One translation file imported.'); + $this->assertTranslation('Tuesday', 'Dienstag', 'de'); + + // Disable and uninstall a module. + $edit = array( + 'modules[Testing][locale_test_translate][enable]' => FALSE, + ); + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + $edit = array( + 'uninstall[locale_test_translate]' => 1, + ); + $this->drupalPost('admin/modules/uninstall', $edit, t('Uninstall')); + $this->drupalPost(NULL, array(), t('Uninstall')); + + // Check if the file data is removed from the database. + $history = locale_translation_get_file_history(); + $this->assertFalse(isset($history['locale_test_translate']), 'Project removed from the file history'); + $projects = locale_translation_get_projects(); + $this->assertFalse(isset($projects['locale_test_translate']), 'Project removed from the project list'); + } + + /** + * Test automatic translation import when a langauge is enabled. + * + * When a language is added, the system will check for translations files of + * enabled modules and will import them. When a language is removed the system + * will remove all translations of that langague from the database. + */ + function testEnableDisableLanguage() { + // Enable a module. + $edit = array( + 'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate', + ); + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + + // Check is there is no Dutch translation yet. + $this->assertTranslation('Extraday', '', 'nl'); + $this->assertTranslation('Tuesday', 'Dienstag', 'de'); + + // Add a language + $edit = array( + 'predefined_langcode' => 'nl', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Check if the right number of translations are added. + $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', + array('%number' => 8, '%update' => 0, '%delete' => 0)), 'One language added.'); + $this->assertTranslation('Extraday', 'extra dag', 'nl'); + + // Check if the language data is added to the database. + $result = db_query("SELECT project FROM {locale_file} WHERE langcode='nl'")->fetchField(); + $this->assertTrue((boolean) $result, 'Files removed from file history'); + + // Remove a language + $this->drupalPost('admin/config/regional/language/delete/nl', array(), t('Delete')); + + // Check if the language data is removed from the database. + $result = db_query("SELECT project FROM {locale_file} WHERE langcode='nl'")->fetchField(); + $this->assertFalse($result, 'Files removed from file history'); + + // Check is there is no more Duth translation. + $this->assertTranslation('Extraday', '', 'nl'); + $this->assertTranslation('Tuesday', 'Dienstag', 'de'); + } + } + +/* + function testAutomaticModuleTranslationImportLanguageEnable() { + // Enable a module. + $edit = array( + 'modules[Testing][locale_test_translate][enable]' => 'locale_test_translate', + ); + $this->drupalPost('admin/modules', $edit, t('Save configuration')); + + // Code for the language - manually set to match the test translation file. + $langcode = 'xx'; + // The English name for the language. + $name = $this->randomName(16); + + // Create a custom language. + $edit = array( + 'predefined_langcode' => 'custom', + 'langcode' => $langcode, + 'name' => $name, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Ensure the translation file was automatically imported when language was + // added. + $this->assertText(t('One translation file imported.'), t('Language file automatically imported.')); + $this->assertText(t('A translation string was skipped because of disallowed or malformed HTML'), t('Language file automatically imported.')); + + // Ensure strings were successfully imported. + $search = array( + 'string' => 'lundi', + 'langcode' => $langcode, + 'translation' => 'translated', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertNoText(t('No strings available.'), t('String successfully imported.')); + + // Ensure multiline string was imported. + $search = array( + 'string' => 'Source string for multiline translation', + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText('Multiline translation string to make sure that import works with it.', t('String successfully imported.')); + + // Ensure 'Allowed HTML source string' was imported but the translation for + // 'Another allowed HTML source string' was not because it contains invalid + // HTML. + $search = array( + 'string' => 'HTML source string', + 'langcode' => $langcode, + 'translation' => 'all', + ); + $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter')); + $this->assertText('Allowed HTML source string', t('String successfully imported.')); + $this->assertNoText('Another allowed HTML source string', t('String with disallowed translation not imported.')); + } +*/ \ No newline at end of file diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc index 1be5fda..7bb35ef 100644 --- a/core/modules/locale/locale.batch.inc +++ b/core/modules/locale/locale.batch.inc @@ -33,14 +33,24 @@ function locale_translation_batch_status_fetch_remote($source, &$context) { $remote_file = $source->files['remote']; $result = locale_translation_http_check($remote_file->uri); - // Update the file object with the result data. In case of a redirect we - // store the resulting uri. - if ($result && !empty($result->updated)) { - $remote_file->uri = isset($result->redirect_uri) ? $result->redirect_uri : $remote_file->uri; - $remote_file->timestamp = $result->updated; - $source->files['remote'] = $remote_file; + if ($result) { + // Update the file object with the result data. In case of a redirect we + // store the resulting uri. If a file is not found we don't update the + // file object, and store it unchanged. + if (isset($result->updated)) { + $remote_file->uri = isset($result->redirect_uri) ? $result->redirect_uri : $remote_file->uri; + $remote_file->timestamp = $result->updated; + $source->files['remote'] = $remote_file; + } + // Record success. + $context['results']['files'][$source->name] = $source->name; + } + else { + // An error occured when checking the file. Record the failure for + // reporting at the end of the batch. + $context['results']['failed_files'][] = $source->name; } - $context['results']['sources'][$source->name][$source->language] = $source; + $context['results']['sources'][$source->name][$source->langcode] = $source; } } @@ -69,10 +79,13 @@ function locale_translation_batch_status_fetch_local($sources, &$context) { // If remote data was collected before, we merge it into the newly // collected result. - if (isset($context['results']['sources'][$source->name][$source->language])) { - $source->files['remote'] = $context['results']['sources'][$source->name][$source->language]->files['remote']; + if (isset($context['results']['sources'][$source->name][$source->langcode])) { + $source->files['remote'] = $context['results']['sources'][$source->name][$source->langcode]->files['remote']; } - $context['results']['sources'][$source->name][$source->language] = $source; + + // Record success and store the updated source data. + $context['results']['files'][$source->name] = $source->name; + $context['results']['sources'][$source->name][$source->langcode] = $source; } } } @@ -83,7 +96,7 @@ function locale_translation_batch_status_fetch_local($sources, &$context) { * 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 + * stored in the 'locale.translation_status' state variable. Other * processes can collect this data after the batch process is completed. * * @param array $context @@ -115,8 +128,8 @@ function locale_translation_batch_status_compare(&$context) { // 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]; + if (isset($history[$source->project][$source->langcode])) { + $current = $history[$source->project][$source->langcode]; // Add the current translation to the source object to save it in // the status cache. $source->files['current'] = $current; @@ -152,14 +165,23 @@ function locale_translation_batch_status_compare(&$context) { function locale_translation_batch_status_finished($success, $results) { $t = get_t(); if ($success) { - if (isset($results['sources']) && $results['sources']) { + if (isset($results['failed_files'])) { + if (module_exists('dblog')) { + $message = format_plural(count($results['failed_files']), 'One translation file could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.', array('@url' => url('admin/reports/dblog'))); + } + else { + $message = format_plural(count($results['failed_files']), 'One translation files could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.'); + } + drupal_set_message($message, 'error'); + } + if (isset($results['files'])) { drupal_set_message(format_plural( count($results['sources']), 'Checked available interface translation updates for one project.', 'Checked available interface translation updates for @count projects.' )); } - else { + if (!isset($results['failed_files']) && !isset($results['files'])) { drupal_set_message(t('Nothing to check.')); } } @@ -183,8 +205,14 @@ function locale_translation_batch_status_finished($success, $results) { * @see locale_translation_batch_fetch_update_status(). * @see locale_translation_batch_status_compare(). */ -function locale_translation_batch_fetch_sources($projects, $languages, &$context) { - $context['results']['input'] = locale_translation_load_sources($projects, $languages); +function locale_translation_batch_fetch_sources($projects, $langcodes, &$context) { + $context['results']['input'] = locale_translation_load_sources($projects, $langcodes); + + // If this batch operation is preceded by the status check operations, the + // results of those operation are stored in the context. We remove them here + // to keep the result records clean. + unset($context['results']['files']); + unset($context['results']['failed_files']); } /** @@ -218,8 +246,6 @@ function locale_translation_batch_fetch_download($project, $langcode, &$context) $source->files['download'] = $file; } else { - // @todo Error handling when download fails. - watchdog('locale', 'Unable to download translation file @uri.', array('@uri' => $source->files['remote']->uri), WATCHDOG_ERROR); $context['results']['failed_files'][] = $source->files['remote']; } $context['results']['sources'][$project][$langcode] = $source; @@ -228,7 +254,7 @@ function locale_translation_batch_fetch_download($project, $langcode, &$context) } /** - * Batch process: Import translation file.. + * Batch process: Import translation file. * * This batch operations imports either a local gettext file or a downloaded * remote gettext file. In case of a downloaded file the location of the @@ -242,10 +268,11 @@ function locale_translation_batch_fetch_download($project, $langcode, &$context) * @param string $langcode * Language code. * @param array $options - * Array of import options. See locale_translate_batch_import_files(). + * Array of import options. * @param $context * Batch context array. * + * @see locale_translate_batch_import_files(). * @see locale_translation_batch_fetch_sources(). * @see locale_translation_batch_fetch_download(). * @see locale_translation_batch_fetch_update_status(). @@ -262,9 +289,9 @@ function locale_translation_batch_fetch_import($project, $langcode, $options, &$ // If we are working on a remote file we will import the downloaded file and // use the result data from the if ($source->type == 'remote') { - if (isset($context['results']['sources'][$source->project][$source->language]->files['download'])) { + if (isset($context['results']['sources'][$source->project][$source->langcode]->files['download'])) { $import_type = 'download'; - $source_result = $context['results']['sources'][$source->project][$source->language]; + $source_result = $context['results']['sources'][$source->project][$source->langcode]; } } else { @@ -272,31 +299,31 @@ function locale_translation_batch_fetch_import($project, $langcode, $options, &$ $source_result = $source; } - // @todo Fault handling. Using try/catch? For other batch functions too? - $file = $source_result->files[$import_type]; module_load_include('bulk.inc', 'locale'); $options += array( 'message' => 'Importing: ' . $file->filename, ); - + // Import the translation file. For large files the batch operations is + // progressive and will be called repeatedly untill finished. locale_translate_batch_import($file, $options, $context); - // Import is finished. + + // The import is finished. if (isset($context['finished']) && $context['finished'] == 1) { - // Import is successfull. + // The import is successfull. if (isset($context['results']['files'][$file->uri])) { $context['message'] = $t('Imported: %name.', array('%name' => $file->filename)); - // Store the data of imported source. This data can later be saved in {locale_file}. + // Keep the data of imported source. In following batch operations + // it will be saved in the {locale_file} table. $source_result->files['import'] = $source_result->files[$source->type]; // Downloaded files are stored in the temporary files directory. On // succesfull import, the files are moved to the local destination - // directory if files should be kept locally. Otherwise the temporary - // files are deleted. + // directory if files should be kept locally. Otherwise the + // temporary files are deleted. if ($import_type == 'download' && variable_get('locale_translate_file_directory', conf_path() . '/files/translations') && isset($source_result->files['local'])) { // Move the temporary file to the local translations directory. - // @todo What to do if the file can not be moved? If it throws an error (watchdog), should we set a batch error? if (file_unmanaged_move($file->uri, $source_result->files['local']->uri, FILE_EXISTS_REPLACE)) { // The downloaded file is now moved to the local file location. // From this point forward we can treat it as if we imported a @@ -328,7 +355,7 @@ function locale_translation_batch_fetch_import($project, $langcode, $options, &$ } } } - $context['results']['sources'][$source->project][$source->language] = $source_result; + $context['results']['sources'][$source->project][$source->langcode] = $source_result; } } } @@ -355,17 +382,18 @@ function locale_translation_batch_fetch_update_status(&$context) { foreach ($context['results']['sources'] as $project => $langcodes) { foreach ($langcodes as $langcode => $source) { - // Store the state of the translation in {locale_file} table. During the - // batch execution the data of imported file is kept in - // $context['results']['sources'] will now be stored in the database. + // Store the state of the imported translations in {locale_file} table. + // During the batch execution data of imported files is temporary stored + // in $context['results']['sources']. Now is will be stored in the + // database. Afterwards the temporary import and download data can be + // deleted. if (isset($source->files['import'])) { $file = $source->files['import']; locale_translation_update_file_history($file); - // @todo Leave unset() disabled for debugging. - //unset($source->files['import']); + unset($source->files['import']); } - // @todo Leave unset() disabled for debugging. - //unset($source->files['download']); + unset($source->files['download']); + // The source data is now up to date. Data of local and/or remote source // file is up to date including an updated time stamp. In a next batch // operation this can be use to update the translation status. @@ -398,13 +426,36 @@ function locale_translation_batch_fetch_finished($success, $results) { * @return stdClass * Result object containing the HTTP request headers, response code, headers, * data, redirect status and updated timestamp. + * TRUE if the file is not found. + * FALSE if a fault occured. */ function locale_translation_http_check($uri, $headers = array()) { $result = drupal_http_request($uri, array('headers' => $headers, 'method' => 'HEAD')); - if ($result && $result->code == '200') { - $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0; + if (!isset($result->error)) { + if ($result->code == 200) { + $result->updated = isset($result->headers['last-modified']) ? strtotime($result->headers['last-modified']) : 0; + } + return $result; } - return $result; + else { + switch ($result->code) { + case 404: + // File not found occurs when a translation file is not yet available + // at the translation server. But also if a custom module or custom + // theme does not define the location of a translation file. By default + // the file is checked at the translation server, but it will not be + // found there. + watchdog('locale', 'File not found: @uri.', array('@uri' => $uri)); + return TRUE; + case 0: + watchdog('locale', 'Error occurred when trying to check @remote: @errormessage.', array('@errormessage' => $result->error, '@remote' => $uri), WATCHDOG_ERROR); + break; + default: + watchdog('locale', 'HTTP error @errorcode occurred when trying to check @remote.', array('@errorcode' => $result->code, '@remote' => $uri), WATCHDOG_ERROR); + break; + } + } + return FALSE; } /** @@ -419,9 +470,9 @@ function locale_translation_http_check($uri, $headers = array()) { * - "langcode": Translation language. * - "version": Project version. * - "filename": File name. + * * @return object - * File object if download successful. - * FALSE on failure. + * File object if download was successful. FALSE on failure. */ function locale_translation_download_source($source_file) { if ($uri = system_retrieve_file($source_file->uri, 'temporary://')) { @@ -434,5 +485,6 @@ function locale_translation_download_source($source_file) { $file->filename = $source_file->filename; return $file; } + watchdog('locale', 'Unable to download translation file @uri.', array('@uri' => $source->files['remote']->uri), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index fd50243..586d626 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -266,112 +266,42 @@ function locale_translate_export_form_submit($form, &$form_state) { } /** - * Sets a batch for a newly-added language. + * Get interface translation files present in the translations directory. * - * @param array $options - * An array with options that can have the following elements: - * - 'langcode': The language code, required. - * - 'overwrite_options': Overwrite options array as defined in - * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array. - * - 'customized': Flag indicating whether the strings imported from $file - * are customized translations or come from a community source. Use - * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to - * LOCALE_NOT_CUSTOMIZED. - * - 'finish_feedback': Whether or not to give feedback to the user when the - * batch is finished. Optional, defaults to TRUE. - */ -function locale_translate_add_language_set_batch($options) { - $options += array( - 'overwrite_options' => array(), - 'customized' => LOCALE_NOT_CUSTOMIZED, - 'finish_feedback' => TRUE, - ); - // See if we have language files to import for the newly added language, - // collect and import them. - if ($batch = locale_translate_batch_import_files($options)) { - batch_set($batch); - } -} - -/** - * Prepare a batch to import all translations. + * @param array $projects + * Project names from which to get the translation files and history. + * Defaults to all projects. + * @param array $langcodes + * Language codes from which to get the translation files and history. + * Defaults to all languagues * - * @param array $options - * An array with options that can have the following elements: - * - 'langcode': The language code. Optional, defaults to NULL, which means - * that the language will be detected from the name of the files. - * - 'overwrite_options': Overwrite options array as defined in - * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array. - * - 'customized': Flag indicating whether the strings imported from $file - * are customized translations or come from a community source. Use - * LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. Optional, defaults to - * LOCALE_NOT_CUSTOMIZED. - * - 'finish_feedback': Whether or not to give feedback to the user when the - * batch is finished. Optional, defaults to TRUE. - * - * @param $force - * (optional) Import all available files, even if they were imported before. - * - * @todo - * Integrate with update status to identify projects needed and integrate - * l10n_update functionality to feed in translation files alike. - * See http://drupal.org/node/1191488. + * @return array + * An array of interface translation files keyed by their URI. */ -function locale_translate_batch_import_files($options, $force = FALSE) { - $options += array( - 'overwrite_options' => array(), - 'customized' => LOCALE_NOT_CUSTOMIZED, - 'finish_feedback' => TRUE, - ); +function locale_translate_get_interface_translation_files($projects = array(), $langcodes = array()) { + module_load_include('compare.inc', 'locale'); $files = array(); - if (!empty($options['langcode'])) { - $langcodes = array($options['langcode']); - } - else { - // If langcode was not provided, make sure to only import files for the - // languages we have enabled. - $langcodes = array_keys(language_list()); - } - foreach ($langcodes as $langcode) { - $files = array_merge($files, locale_translate_get_interface_translation_files($langcode)); - } - if (!$force) { - $result = db_select('locale_file', 'lf') - ->fields('lf', array('langcode', 'uri', 'timestamp')) - ->condition('langcode', $langcodes) - ->execute() - ->fetchAllAssoc('uri'); - foreach ($result as $uri => $info) { - if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) { - // The file is already imported and not changed since the last import. - // Remove it from file list and don't import it again. - unset($files[$uri]); + $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + + // Scan the translations directory for files matching a name pattern + // containing a project name and language code: {project}.{langcode}.po or + // {project}-{version}.{langcode}.po. + // Only files of known projects and languages will be returned. + if ($directory = variable_get('locale_translate_file_directory', conf_path() . '/files/translations')) { + $result = file_scan_directory($directory, '![a-z_]+(\-[0-9a-z\.\-\+]+|)\.[^\./]+\.po$!', array('recurse' => FALSE)); + + foreach ($result as $filepath => $file) { + // Update the file object with project name and version from the file name. + $file = locale_translate_file_attach_properties($file); + if (in_array($file->project, $projects)) { + if (in_array($file->langcode, $langcodes)) { + $files[$file->uri] = $file; + } } } } - return locale_translate_batch_build($files, $options); -} - -/** - * Get an array of available interface translation file. - * - * @param $langcode - * The langcode for the interface translation files. Pass NULL to get all - * available interface translation files. - * - * @return array - * An array of interface translation files. - */ -function locale_translate_get_interface_translation_files($langcode = NULL) { - $directory = variable_get('locale_translate_file_directory', conf_path() . '/files/translations'); - $return = file_scan_directory($directory, '!' . (!empty($langcode) ? '\.' . preg_quote($langcode, '!') : '') . '\.po$!', array('recurse' => FALSE)); - - foreach ($return as $filepath => $file) { - $file->uri = 'translations://' . $file->filename; - $return[$file->uri] = $file; - unset($return[$filepath]); - } - return $return; + return $files; } /** @@ -430,16 +360,14 @@ function locale_translate_batch_build($files, $options) { /** * Perform interface translation import as a batch step. * - * @todo Rework description. - * The given filepath is matched against ending with '{langcode}.po'. When - * matched the filepath is added to batch context. - * - * @param $file - * File object ... @todo + * @param object $file + * A file object of the gettext file to be imported. The file object must + * contain a language parameter (other than LANGUAGE_NOT_SPECIFIED). This + * is used as the language of the import. * * @param array $options * An array with options that can have the following elements: - * - 'langcode': The language code, required. + * - 'langcode': The language code. * - 'overwrite_options': Overwrite options array as defined in * Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array. * - 'customized': Flag indicating whether the strings imported from $file @@ -457,6 +385,7 @@ function locale_translate_batch_import($file, $options, &$context) { 'overwrite_options' => array(), 'customized' => LOCALE_NOT_CUSTOMIZED, ); + if (isset($file->langcode) && $file->langcode != LANGUAGE_NOT_SPECIFIED) { try { @@ -497,25 +426,24 @@ function locale_translate_batch_import($file, $options, &$context) { $context['results']['files'][$file->uri] = $file; $context['results']['languages'][$file->uri] = $file->langcode; } - // Add the values from the report to the stats for this file. + + // Add the reported values to the statistics for this file. + // Each import itteration reports statistics in an array. The results of + // each itteration are added and merged here and stored per file. if (!isset($context['results']['stats']) || !isset($context['results']['stats'][$file->uri])) { $context['results']['stats'][$file->uri] = array(); } foreach ($report as $key => $value) { - if (is_numeric($value)) { - $context['results']['stats'][$file->uri] += array($key => 0); - $context['results']['stats'][$file->uri][$key] += $value; - } - elseif (is_array($value)) { - $context['results']['stats'][$file->uri] += array($key => array()); - $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value); - } if (is_numeric($report[$key])) { if (!isset($context['results']['stats'][$file->uri][$key])) { $context['results']['stats'][$file->uri][$key] = 0; } $context['results']['stats'][$file->uri][$key] += $report[$key]; } + elseif (is_array($value)) { + $context['results']['stats'][$file->uri] += array($key => array()); + $context['results']['stats'][$file->uri][$key] = array_merge($context['results']['stats'][$file->uri][$key], $value); + } } } catch (Exception $exception) { @@ -533,9 +461,17 @@ function locale_translate_batch_import($file, $options, &$context) { * Contains a list of imported files. */ function locale_translate_batch_import_save($context) { - foreach ($context['results']['files'] as $file) { - $file->last_checked = REQUEST_TIME; - locale_translation_update_file_history($file); + if (isset($context['results']['files'])) { + foreach ($context['results']['files'] as $file) { + // Update the file history if both project and version are known. This table + // is used by the automated translation update function which tracks + // translation status of module and themes in the system. Other data + // than installed projects would stay in the table for ever never. + if ($file->project && $file->version) { + $file->last_checked = REQUEST_TIME; + locale_translation_update_file_history($file); + } + } } } @@ -556,8 +492,6 @@ function locale_translate_batch_finished($success, $results) { drupal_set_message($message, 'error'); } if (isset($results['files'])) { - drupal_set_message(format_plural(count($results['files']), 'One translation file imported.', '@count translation files imported.')); - $skipped_files = array(); // If there are no results and/or no stats (eg. coping with an empty .po // file), simply do nothing. @@ -576,16 +510,21 @@ function locale_translate_batch_finished($success, $results) { $strings = array_unique($strings); $langcodes = array_unique(array_values($results['languages'])); } - drupal_set_message(t('The translations were successfully imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes))); - watchdog('locale', 'The translations were successfully imported. %number translations added, %update updated and %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)); + drupal_set_message(format_plural(count($results['files']), + 'One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', + '@count translation files imported. %number translations were added, %update translations were updated and %delete translations were removed.', + array('%number' => $additions, '%update' => $updates, '%delete' => $deletes) + )); + watchdog('locale', 'Translations imported: %number added, %update updated, %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)); + if ($skips) { if (module_exists('dblog')) { - $message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => url('admin/reports/dblog'))); + $message = format_plural($skips, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', array('@url' => url('admin/reports/dblog'))); } else { - $message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.'); + $message = format_plural($skips, 'One translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.'); } - drupal_set_message($message, 'error'); + drupal_set_message($message, 'warning'); watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING); } @@ -617,26 +556,42 @@ function locale_translate_file_create($filepath) { /** * Generate file properties from filename and options. * - * @param $file + * An attempt is made to determine the translation language, project name and + * project version from the file name. Supported file name patterns are: + * {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po. + * Alternatively the translation language can be set using the $options. + * + * @param object $file + * A file object of the gettext file to be imported. * @param array $options + * An array with options: + * - 'langcode': The language code. Overrides the file language. * - * @return mixed + * @return object + * Modified file object. */ function locale_translate_file_attach_properties($file, $options = array()) { - // @todo: Check if regex works with missing project and version. - // @todo: move the language part to the form submit function? - // The filename is either {langcode}.po or {prefix}.{langcode}.po. - // Try to get the project name, project version and language code file name. - // The filename is either {langcode}.po or {project}-{version}.{langcode}.po. - // We use the language from either the $options or the file name. - preg_match('!([a-z_]+)\-([0-9a-z\.\-\+]+)\.([^\./]+)\.po$!', $file->filename, $matches); - $file->project = isset($matches[1]) ? $matches[1] : ''; - $file->version = isset($matches[2]) ? $matches[2] : ''; - if (isset($options['langcode'])) { - $file->langcode = $options['langcode']; - } - elseif (isset($matches[3])) { - $file->langcode = $matches[3]; + // Extract project, verion and language code from the file name. Supported: + // {project}-{version}.{langcode}.po, {prefix}.{langcode}.po or {langcode}.po + preg_match('! + ( # project OR project and version OR emtpy (group 1) + ([a-z_]+) # project name (group 2) + \. # . + | # OR + ([a-z_]+) # project name (group 3) + \- # - + ([0-9a-z\.\-\+]+) # version (group 4) + \. # . + | # OR + ) # (empty) + ([^\./]+) # language code (group 5) + \. # . + po # po extension + $!x', $file->filename, $matches); + if (isset($matches[5])) { + $file->project = $matches[2] . $matches[3]; + $file->version = $matches[4]; + $file->langcode = isset($options['langcode']) ? $options['langcode'] : $matches[5]; } else { $file->langcode = LANGUAGE_NOT_SPECIFIED; @@ -645,28 +600,32 @@ function locale_translate_file_attach_properties($file, $options = array()) { } /** - * Deletes all interface translation files depending on the langcode. + * Deletes interface translation files and translation history records. * - * @param $langcode - * A langcode or NULL. Pass NULL to delete all interface translation files. + * @param array $projects + * Project names from which to delete the translation files and history. + * Defaults to all projects. + * @param array $langcodes + * Language codes from which to delete the translation files and history. + * Defaults to all languagues + * + * @return boolean + * TRUE if files are removed sucessfully. FALSE if one or more files could + * not be deleted. */ -function locale_translate_delete_translation_files($langcode) { - $files = locale_translate_get_interface_translation_files($langcode); - $return = TRUE; - if (!empty($files)) { +// @todo Move this function to translation.inc? +function locale_translate_delete_translation_files($projects = array(), $langcodes = array()) { + $fail = FALSE; + locale_translation_file_history_delete($projects, $langcodes); + + // Delete all translation files from the translations directory. + if ($files = locale_translate_get_interface_translation_files($projects, $langcodes)) { foreach ($files as $file) { $success = file_unmanaged_delete($file->uri); if (!$success) { - $return = FALSE; - } - else { - // Remove the registered translation file if any. - db_delete('locale_file') - ->condition('langcode', $langcode) - ->condition('uri', $file->uri) - ->execute(); + $fail = TRUE; } } } - return $return; + return !$fail; } diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index a604082..333ee53 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -16,6 +16,8 @@ * Clear the project data table. */ function locale_translation_flush_projects() { + // Followup issue: http://drupal.org/node/1842362 + // Replace {locale_project} table by state() variable(s). db_truncate('locale_project')->execute(); } @@ -113,6 +115,9 @@ function locale_translation_build_projects() { 'status' => $project->status, )) ->execute(); + + // Invalidate the cache of translatable projects. + locale_translation_clear_cache_projects(); } return $projects; } @@ -220,7 +225,7 @@ function locale_translation_build_server_pattern($project, $template) { '%project' => $project->name, '%version' => $project->version, '%core' => $project->core, - '%language' => isset($project->language) ? $project->language : '%language', + '%language' => isset($project->langcode) ? $project->langcode : '%language', ); return strtr($template, $variables); } @@ -237,7 +242,6 @@ function locale_translation_build_server_pattern($project, $template) { * Available sources indexed by project and language. */ function locale_translation_check_projects($projects = array(), $langcodes = array()) { -// @todo What happens if LOCALE_TRANSLATION_USE_SOURCE_LOCAL and no local file is available?? if (config('locale.settings')->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL) { // Retrieve the status of both remote and local translation sources by // using a batch process. @@ -255,7 +259,7 @@ function locale_translation_check_projects($projects = array(), $langcodes = arr * 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'. + * 'locale.translation_status'. * * @param array $projects * Array of project names to check. Defaults to all translatable projects. @@ -274,23 +278,23 @@ function locale_translation_check_projects_batch($projects = array(), $langcodes * The batch process fetches the state of both remote and (if configured) local * translation files. The data of the most recent translation is stored per * per project and per language. This data is stored in a state variable - * 'locale_translation_status'. The timestamp it was last updated is stored - * in the state variable 'locale_translation_status_last_update'. + * 'locale.translation_status'. The timestamp it was last updated is stored + * in the state variable 'locale.translation_last_checked'. * * @param array $projects * Array of project names for which to check the state of translation files. * Defaults to all translatable projects. - * @param array $languages + * @param array $langcodes * Array of language codes. Defaults to all translatable languages. * * @return array * Batch definition array. */ -function locale_translation_batch_status_build($projects = array(), $languages = array()) { +function locale_translation_batch_status_build($projects = array(), $langcodes = array()) { $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); - $languages = $languages ? $languages : array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); - $operations = _locale_translation_batch_status_operations($projects, $languages); + $operations = _locale_translation_batch_status_operations($projects, $langcodes); $batch = array( 'operations' => $operations, @@ -335,7 +339,7 @@ function _locale_translation_batch_status_operations($projects, $langcodes) { * * Only po files in the local file system are checked. Any remote translation * sources will be ignored. Results are stored in the state variable - * 'locale_translation_status'. + * 'locale.translation_status'. * * 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 @@ -346,12 +350,12 @@ function _locale_translation_batch_status_operations($projects, $langcodes) { * @param array $projects * Array of project names for which to check the state of translation files. * Defaults to all translatable projects. - * @param array $languages + * @param array $langcodes * Array of language codes. Defaults to all translatable languages. */ -function locale_translation_check_projects_local($projects = array(), $languages = array()) { +function locale_translation_check_projects_local($projects = array(), $langcodes = array()) { $projects = locale_translation_get_projects($projects); - $languages = $languages ? $languages : array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); $history = locale_translation_get_file_history(); $results = array(); @@ -359,7 +363,7 @@ function locale_translation_check_projects_local($projects = array(), $languages // available. When found the source object is updated with the appropriate // type and timestamp of the po file. foreach ($projects as $name => $project) { - foreach ($languages as $langcode) { + foreach ($langcodes as $langcode) { $source = locale_translation_source_build($project, $langcode); if (locale_translation_source_check_file($source)) { $source->type = 'local'; @@ -370,8 +374,8 @@ function locale_translation_check_projects_local($projects = array(), $languages // 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]; + if (isset($history[$source->project][$source->langcode])) { + $current = $history[$source->project][$source->langcode]; // Add the current translation to the source object to save it in // the status cache. $source->files['current'] = $current; diff --git a/core/modules/locale/locale.fetch.inc b/core/modules/locale/locale.fetch.inc index a6f2792..e5278e7 100755 --- a/core/modules/locale/locale.fetch.inc +++ b/core/modules/locale/locale.fetch.inc @@ -16,7 +16,7 @@ * @param array $projects * Array of project names for which to update the translations. Defaults to * all translatable projects. - * @param array $languages + * @param array $langcodes * Array of language codes. Defaults to all translatable languages. * @param array $options * Array of import options. See locale_translate_batch_import_files(). @@ -24,13 +24,15 @@ * @return array * Batch definition array. */ -function locale_translation_batch_update_build($projects = array(), $languages = array(), $options = array()) { +function locale_translation_batch_update_build($projects = array(), $langcodes = array(), $options = array()) { + // @todo Merge compare.inc and fetch.inc? Merge with batch.inc? + module_load_include('compare.inc', 'locale'); + $t = get_t(); $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); - $languages = $languages ? $languages : array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); - $t = get_t(); - $operations = _locale_translation_batch_status_operations($projects, $languages); - $operations = array_merge($operations, _locale_translation_fetch_operations($projects, $languages, $options)); + $operations = _locale_translation_batch_status_operations($projects, $langcodes); + $operations = array_merge($operations, _locale_translation_fetch_operations($projects, $langcodes, $options)); $batch = array( 'operations' => $operations, @@ -49,7 +51,7 @@ function locale_translation_batch_update_build($projects = array(), $languages = * @param array $projects * Array of project names for which to check the state of translation files. * Defaults to all translatable projects. - * @param array $languages + * @param array $langcodes * Array of language codes. Defaults to all translatable languages. * @param array $options * Array of import options. See locale_translate_batch_import_files(). @@ -57,13 +59,13 @@ function locale_translation_batch_update_build($projects = array(), $languages = * @return array * Batch definition array. */ -function locale_translation_batch_fetch_build($projects = array(), $languages = array(), $options = array()) { +function locale_translation_batch_fetch_build($projects = array(), $langcodes = array(), $options = array()) { $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); - $languages = $languages ? $languages : array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); $t = get_t(); $batch = array( - 'operations' => _locale_translation_fetch_operations($projects, $languages, $options), + 'operations' => _locale_translation_fetch_operations($projects, $langcodes, $options), 'title' => $t('Updating translation.'), 'init_message' => $t('Downloading and importing files.'), 'error_message' => $t('Error importing interface translations'), @@ -79,7 +81,7 @@ function locale_translation_batch_fetch_build($projects = array(), $languages = * @param array $projects * Array of project names for which to check the state of translation files. * Defaults to all translatable projects. - * @param array $languages + * @param array $langcodes * Array of language codes. Defaults to all translatable languages. * @param array $options * Array of import options. @@ -87,14 +89,14 @@ function locale_translation_batch_fetch_build($projects = array(), $languages = * @return array * Array of batch operations. */ -function _locale_translation_fetch_operations($projects, $languages, $options) { +function _locale_translation_fetch_operations($projects, $langcodes, $options) { $operations = array(); $config = config('locale.settings'); - $operations[] = array('locale_translation_batch_fetch_sources', array($projects, $languages)); + $operations[] = array('locale_translation_batch_fetch_sources', array($projects, $langcodes)); foreach ($projects as $project) { - foreach ($languages as $langcode) { + foreach ($langcodes as $langcode) { $operations[] = array('locale_translation_batch_fetch_download', array($project, $langcode)); $operations[] = array('locale_translation_batch_fetch_import', array($project, $langcode, $options)); } diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index f53ec9b..db230ff 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -251,7 +251,7 @@ function locale_stream_wrappers() { 'name' => t('Translation files'), 'class' => 'Drupal\locale\TranslationsStream', 'description' => t('Translation files'), - 'type' => STREAM_WRAPPERS_LOCAL_NORMAL, + 'type' => STREAM_WRAPPERS_LOCAL_HIDDEN, ), ); return $wrappers; @@ -288,11 +288,10 @@ function locale_language_delete($language) { // Remove interface translation files. module_load_include('inc', 'locale', 'locale.bulk'); - locale_translate_delete_translation_files($language->langcode); - - _locale_invalidate_js($language->langcode); + locale_translate_delete_translation_files(array(), array($language->langcode)); // Changing the language settings impacts the interface: + _locale_invalidate_js($language->langcode); cache('page')->flush(); // Clearing all locale cache from database @@ -448,6 +447,13 @@ function locale_modules_installed($modules) { } /** + * Implements hook_modules_uninstalled(). + */ +function locale_modules_uninstalled($modules) { + locale_system_remove($modules); +} + +/** * Implements hook_themes_enabled(). * * @todo This is technically wrong. We must not import upon enabling, but upon @@ -458,34 +464,80 @@ function locale_themes_enabled($themes) { } /** + * Implements hook_themes_disabled(). + */ +function locale_themes_disabled($themes) { + locale_system_remove($themes); +} + +/** * Imports translations when new modules or themes are installed. * - * This function will either import translation for the component change - * right away, or start a batch if more files need to be imported. + * This function will start a batch to import translations for the added + * components. * - * @param $components + * @param array $components * An array of component (theme and/or module) names to import * translations for. - * - * @todo - * This currently imports all .po files available, independent of - * $components. Once we integrated with update status for project - * identification, we should revisit and only import files for the - * identified projects for the components. */ function locale_system_update($components) { // Skip running the translation imports if in the installer, // because it would break out of the installer flow. We have // built-in support for translation imports in the installer. if (!drupal_installation_attempted()) { - include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc'; - if ($batch = locale_translate_batch_import_files(array(), TRUE)) { + module_load_include('compare.inc', 'locale'); + + // Update the list of translatable projects and start the import batch. + // Only when new projects are added the update batch will be triggered. Not + // each enabled module will introduce a new project. E.g. sub modules. + $projects = array_keys(locale_translation_build_projects()); + if ($components = array_intersect($components, $projects)) { + module_load_include('fetch.inc', 'locale'); + // Get translation status of the projects, download and update translations. + $options = _locale_translation_default_update_options(); + $batch = locale_translation_batch_update_build($components, array(), $options); batch_set($batch); } } } /** + * Delete translation history of modules and themes. + * + * Only the translation history is removed, not the source strings or + * translations. This is not possible because strings are shared between + * modules and we have no record of which string is used by which module. + * + * @param array $components + * An array of component (theme and/or module) names to remove + * translation history. + */ +function locale_system_remove($components) { + module_load_include('compare.inc', 'locale'); + + // Only when projects are removed, the translation files and records will be + // deleted. Not each disabled module will remove a project. E.g. sub modules. + $projects = array_keys(locale_translation_get_projects()); + if ($components = array_intersect($components, $projects)) { + locale_translation_file_history_delete($components); + + // Remove translation files. + module_load_include('inc', 'locale', 'locale.bulk'); + locale_translate_delete_translation_files($components, array()); + + // Remove translatable projects. + // Followup issue http://drupal.org/node/1842362 to replace the + // {locale_project} table. Then change this to a function call. + db_delete('locale_project') + ->condition('name', $components) + ->execute(); + + // Clear the translation status. + locale_translation_clear_status(); + } +} + +/** * Implements hook_js_alter(). * * This function checks all JavaScript files currently added via drupal_add_js() @@ -676,8 +728,11 @@ function locale_form_language_admin_add_form_alter_submit($form, $form_state) { $langcode = $form_state['values']['predefined_langcode']; } - include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc'; - locale_translate_add_language_set_batch(array('langcode' => $langcode)); + // Download and import translations for the newly added language. + module_load_include('fetch.inc', 'locale'); + $options = _locale_translation_default_update_options(); + $batch = locale_translation_batch_update_build(array(), array($langcode), $options); + batch_set($batch); } /** @@ -813,13 +868,21 @@ function locale_translation_update_file_history($file) { /** * Delete the history of downloaded translations. * - * @param string $langcode - * Language code of the file history to be deleted. + * @param array $projects + * Project name(s) to be deleted from the file history. If both project(s) and + * language code(s) are specified the conditions will be ANDed. + * @param array $langcode + * Language code(s) to be deleted from the file history. */ -function locale_translation_file_history_delete($langcode) { - db_delete('locale_file') - ->condition('language', $langcode) - ->execute(); +function locale_translation_file_history_delete($projects = array(), $langcodes = array()) { + $query = db_delete('locale_file'); + if (!empty($projects)) { + $query->condition('project', $projects); + } + if (!empty($langcodes)) { + $query->condition('langcode', $langcodes); + } + $query->execute(); } /** @@ -829,7 +892,10 @@ function locale_translation_file_history_delete($langcode) { * Array of translation source data, structured by project name and langcode. */ function locale_translation_status_save($data) { - $status = state()->get('locale_translation_status'); + // Followup issue: http://drupal.org/node/1842362 + // Split status storage per module/language and expire individually. This will + // improve performance for large sites. + $status = state()->get('locale.translation_status'); $status = empty($status) ? array() : $status; // Merge the new data into the existing structured status array. @@ -839,16 +905,16 @@ function locale_translation_status_save($data) { } } - state()->set('locale_translation_status', $status); - state()->set('locale_translation_status_last_update', REQUEST_TIME); + state()->set('locale.translation_status', $status); + state()->set('locale.translation_last_checked', REQUEST_TIME); } /** * Clear the translation status cache. */ function locale_translation_clear_status() { - state()->delete('locale_translation_status'); - state()->delete('locale_translation_status_last_update'); + state()->delete('locale.translation_status'); + state()->delete('locale.translation_last_checked'); } /** diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index f01a6ab..f0c05e6 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -455,11 +455,14 @@ function locale_translate_edit_form_submit($form, &$form_state) { function locale_translation_manual_status() { module_load_include('compare.inc', 'locale'); - // @todo Do we need to clear the cache here before loading the projects? + // Check translation status of all translatable project in all languages. + // First we clear the cached list of projects. Although not strictly + // nescessary, this is helpfull in case the project list is out of sync. locale_translation_flush_projects(); locale_translation_check_projects(); - // Execute a batch if required. + // Execute a batch if required. A batch is only used when remote files + // are checked. if (batch_get()) { batch_process('admin/reports/translations'); } @@ -483,7 +486,7 @@ function locale_translate_settings($form, &$form_state) { '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.'), + '#description' => t('Select how frequently you want to check for new interface translations for your currently installed modules and themes. Check updates now.', array('@url' => url('admin/reports/translations/check'))), ); $form['check_disabled_modules'] = array( @@ -504,8 +507,8 @@ function locale_translate_settings($form, &$form_state) { '#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'), + LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL => t('Drupal translation server and local files'), + LOCALE_TRANSLATION_USE_SOURCE_LOCAL => t('Local files only'), ), '#description' => t('The source of translation files for automatic interface translation.') . ' ' . $description, ); @@ -535,7 +538,9 @@ function locale_translate_settings($form, &$form_state) { } /** - * @todo + * Form validation handler for locale_translate_settings(). + * + * @see locale_translate_settings() */ function locale_translate_settings_validate($form, &$form_state) { if (empty($form['#translation_directory']) && $form_state['values']['use_source'] == LOCALE_TRANSLATION_USE_SOURCE_LOCAL) { @@ -544,7 +549,9 @@ function locale_translate_settings_validate($form, &$form_state) { } /** - * @todo + * Form submission handler for locale_translate_settings(). + * + * @see locale_translate_settings() */ function locale_translate_settings_submit($form, &$form_state) { $values = $form_state['values']; @@ -588,7 +595,7 @@ function locale_translation_status_form($form, &$form_state) { //locale_translation_flush_projects(); $languages = locale_translatable_language_list(); - $status = state()->get('locale_translation_status'); + $status = state()->get('locale.translation_status'); if (!$languages) { drupal_set_message(t('No translatable languages available. Add language first.', array('@add_lanuage' => url('admin/config/regional/language'))), 'warning'); @@ -617,24 +624,16 @@ function locale_translation_status_form($form, &$form_state) { */ function locale_translation_status_form_submit($form, &$form_state) { module_load_include('fetch.inc', 'locale'); - $config = config('locale.settings'); - $langcodes = array_filter($form_state['values']['langcodes']); - // Set the batch import options. - // @todo move to helper function? - $options = array( - 'customized' => LOCALE_NOT_CUSTOMIZED, - 'overwrite_options' => array( - 'not_customized' => $config->get('translation.overwrite_not_customized'), - 'customized' => $config->get('translation.overwrite_customized'), - ), - ); + // Set the translation import options. This determines if existing + // translations will be overwritten by imported strings. + $options = _locale_translation_default_update_options(); // If the status was updated recently we can immediately start fetching the // translation updates. If the status is expired we clear it an run a batch to // update the status and then fetch the translation updates. - $last_checked = state()->get('locale_translation_status_last_update'); + $last_checked = state()->get('locale.translation_last_checked'); if ($last_checked < REQUEST_TIME - LOCALE_TRANSLATION_STATUS_TTL) { locale_translation_clear_status(); $batch = locale_translation_batch_update_build(array(), $langcodes, $options); diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc index a397001..a250e4d 100644 --- a/core/modules/locale/locale.translation.inc +++ b/core/modules/locale/locale.translation.inc @@ -6,17 +6,6 @@ */ /** - * Threshold for timestamp comparison. - * - * Eliminates a difference between the download time and the actual .po file - * timestamp in seconds. The download time is stored in the database in - * {locale_file}.timestamp. - */ -// @todo Is this still required since we use filemtime() to get the timestamp of -// downloaded and imported files? -const LOCALE_TRANSLATION_TIMESTAMP_THRESHOLD = 2; - -/** * Comparison result of source files timestamps. * * Timestamp of source 1 is less than the timestamp of source 2. @@ -71,6 +60,7 @@ function locale_translation_get_projects($project_names = array()) { // http://drupal.org/node/1777106 is a follow-up issue to make the check for // possible out-of-date project information more robust. if ($result->rowCount() == 0 && module_exists('update')) { + module_load_include('compare.inc', 'locale'); // At least the core project should be in the database, so we build the // data if none are found. locale_translation_build_projects(); @@ -90,29 +80,36 @@ function locale_translation_get_projects($project_names = array()) { } /** + * Clear the projects cache. + */ +function locale_translation_clear_cache_projects() { + drupal_static('locale_translation_get_projects', array()); +} + +/** * Load cached translation sources containing current translation status. * * @param array $projects * Array of project names. Defaults to all translatable projects. - * @param array $languages + * @param array $langcodes * Array of language codes. Defaults to all translatable languages. * * @return array - * Array of source objects. Keyed by :. + * Array of source objects. Keyed with :. * * @see locale_translation_source_build(). */ -function locale_translation_load_sources($projects = NULL, $languages = NULL) { +function locale_translation_load_sources($projects = NULL, $langcodes = NULL) { $sources = array(); $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); - $languages = $languages ? $languages : array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); // Load source data from locale_translation_status cache. - $status = state()->get('locale_translation_status'); + $status = state()->get('locale.translation_status'); // Use only the selected projects and languages for update. foreach($projects as $project) { - foreach ($languages as $langcode) { + foreach ($langcodes as $langcode) { $sources[$project . ':' . $langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL; } } @@ -124,22 +121,23 @@ function locale_translation_load_sources($projects = NULL, $languages = NULL) { * * @param array $projects * Array of project names. Defaults to all translatable projects. - * @param array $languages + * @param array $langcodes * Array of language codes. Defaults to all translatable languages. * * @return array - * Array of source objects. Keyed by :. + * Array of source objects. Keyed with :. * * @see locale_translation_source_build(). */ -function locale_translation_build_sources($projects = array(), $languages = array()) { +function locale_translation_build_sources($projects = array(), $langcodes = array()) { + $sources = array(); $projects = locale_translation_get_projects($projects); - $languages = $languages ? $languages : array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); foreach ($projects as $project) { - foreach ($languages as $langcode) { + foreach ($langcodes as $langcode) { $source = locale_translation_source_build($project, $langcode); - $sources[$source->name . ':' . $source->language] = $source; + $sources[$source->name . ':' . $source->langcode] = $source; } } return $sources; @@ -229,11 +227,14 @@ function locale_translation_source_check_file(&$source) { * - "keep": TRUE to keep the downloaded file. */ function locale_translation_source_build($project, $langcode, $filename = NULL) { + // Followup issue: http://drupal.org/node/1842380 + // Convert $source object to a TranslatableProject class and use a typed class + // for $source-file. + // Create a source object with data of the project object. $source = clone $project; $source->project = $project->name; - // @todo Change this to langcode. - $source->language = $langcode; + $source->langcode = $langcode; $filename = $filename ? $filename : config('locale.settings')->get('translation.default_filename'); @@ -317,7 +318,7 @@ function _locale_translation_file_is_remote($uri) { */ function _locale_translation_source_compare($source1, $source2) { if (isset($source1->timestamp) && isset($source2->timestamp)) { - if (abs($source1->timestamp - $source2->timestamp) < LOCALE_TRANSLATION_TIMESTAMP_THRESHOLD) { + if ($source1->timestamp == $source2->timestamp) { return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ; } else { @@ -334,3 +335,20 @@ function _locale_translation_source_compare($source1, $source2) { return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ; } } + +/** + * Return default import options for translation update. + * + * @return array + * Array of translation import options. + */ +function _locale_translation_default_update_options() { + $config = config('locale.settings'); + return array( + 'customized' => LOCALE_NOT_CUSTOMIZED, + 'overwrite_options' => array( + 'not_customized' => $config->get('translation.overwrite_not_customized'), + 'customized' => $config->get('translation.overwrite_customized'), + ), + ); +} \ No newline at end of file diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.install b/core/modules/locale/tests/modules/locale_test/locale_test.install index 8f04e0f..3d9dff3 100644 --- a/core/modules/locale/tests/modules/locale_test/locale_test.install +++ b/core/modules/locale/tests/modules/locale_test/locale_test.install @@ -10,6 +10,6 @@ */ function locale_test_uninstall() { // Clear variables. - state()->delete('locale_translation_test_system_info_alter'); - state()->delete('locale_translation_test_projects'); + state()->delete('locale.test_system_info_alter'); + state()->delete('locale.test_projects_alter'); } 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 e084915..a77472f 100644 --- a/core/modules/locale/tests/modules/locale_test/locale_test.module +++ b/core/modules/locale/tests/modules/locale_test/locale_test.module @@ -16,7 +16,7 @@ function locale_test_system_info_alter(&$info, $file, $type) { // By default the locale_test modules are hidden and have a project specified. // To test the module detection proces by locale_project_list() the // test modules should mimic a custom module. I.e. be non-hidden. - if (state()->get('locale_translation_test_system_info_alter')) { + if (state()->get('locale.test_system_info_alter')) { if ($file->name == 'locale_test' || $file->name == 'locale_test_disabled') { // Don't hide the module. $info['hidden'] = FALSE; @@ -33,11 +33,11 @@ function locale_test_system_info_alter(&$info, $file, $type) { * 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 + * The "locale.test_projects_alter" 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')) { + if (state()->get('locale.test_projects_alter')) { // Instead of the default ftp.drupal.org we use the file system of the test // instance to simulate a remote file location. diff --git a/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info new file mode 100644 index 0000000..5a68a07 --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info @@ -0,0 +1,9 @@ +name = Locale test translate +description = Translation test module for locale module testing. +package = Testing +version = 1.3 +core = 8.x + +; Definitions for interface translations. +interface translation project = locale_test_translate +interface translation server pattern = core/modules/locale/tests/test.%language.po diff --git a/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module new file mode 100644 index 0000000..ede02fb --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module @@ -0,0 +1,6 @@ + 1);\\n" + +msgid "Monday" +msgstr "maandag" + +msgid "Tuesday" +msgstr "dinsdag" + +msgid "Wednesday" +msgstr "woensdag" + +msgid "Thursday" +msgstr "donderdag" + +msgid "Extraday" +msgstr "extra dag" + +msgid "Friday" +msgstr "vrijdag" + +msgid "Saturday" +msgstr "zaterdag" + +msgid "Sunday" +msgstr "zondag"