diff --git a/core/modules/locale/lib/Drupal/locale/Form/LocaleSettingsForm.php b/core/modules/locale/lib/Drupal/locale/Form/LocaleSettingsForm.php index 4236bf2..5669d8f 100644 --- a/core/modules/locale/lib/Drupal/locale/Form/LocaleSettingsForm.php +++ b/core/modules/locale/lib/Drupal/locale/Form/LocaleSettingsForm.php @@ -32,8 +32,8 @@ public function buildForm(array $form, array &$form_state) { '#default_value' => $config->get('translation.update_interval_days'), '#options' => array( '0' => t('Never (manually)'), - '1' => t('Daily'), '7' => t('Weekly'), + '30' => t('Monthly'), ), '#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'))), ); diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateBase.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateBase.php new file mode 100644 index 0000000..2b75e60 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateBase.php @@ -0,0 +1,296 @@ +timestamp_old = REQUEST_TIME - 300; + $this->timestamp_medium = REQUEST_TIME - 200; + $this->timestamp_new = REQUEST_TIME - 100; + $this->timestamp_now = REQUEST_TIME; + } + + /** + * Sets the value of the default translations directory. + * + * @param string $path + * Path of the translations directory relative to the drupal installation + * directory. + */ + protected function setTranslationsDirectory($path) { + $this->tranlations_directory = $path; + file_prepare_directory($path, FILE_CREATE_DIRECTORY); + config('locale.settings')->set('translation.path', $path)->save(); + } + + /** + * Adds a language. + * + * @param $langcode + * The language code of the language to add. + */ + protected 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 + * Path of the file relative to the public file path. + * @param string $filename + * Name of the file to create. + * @param integer $timestamp + * Timestamp to set the file to. Defaults to current time. + * @param array $translations + * Array of source/target value translation strings. Only singular strings + * are supported, no plurals. No double quotes are allowed in source and + * translations strings. + */ + protected function makePoFile($path, $filename, $timestamp = NULL, $translations = array()) { + $timestamp = $timestamp ? $timestamp : REQUEST_TIME; + $path = 'public://' . $path; + $text = ''; + $po_header = << 1);\\n" + +EOF; + + // Convert array of translations to Gettext source and translation strings. + if ($translations) { + foreach ($translations as $source => $target) { + $text .= 'msgid "'. $source . '"' . "\n"; + $text .= 'msgstr "'. $target . '"' . "\n"; + } + } + + file_prepare_directory($path, FILE_CREATE_DIRECTORY); + $file = entity_create('file', array( + 'uid' => 1, + 'filename' => $filename, + 'uri' => $path . '/' . $filename, + 'filemime' => 'text/x-gettext-translation', + 'timestamp' => $timestamp, + 'status' => FILE_STATUS_PERMANENT, + )); + file_put_contents($file->uri, $po_header . $text); + touch(drupal_realpath($file->uri), $timestamp); + $file->save(); + } + + /** + * Setup the environment containing local and remote translation 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. + */ + protected 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. + \Drupal::state()->set('locale.test_projects_alter', TRUE); + + // Setup the environment. + $public_path = variable_get('file_public_path', conf_path() . '/files'); + $this->setTranslationsDirectory($public_path . '/local'); + $config->set('translation.default_filename', '%project-%version.%language._po')->save(); + + // Setting up sets of translations for the translation files. + $translations_one = array('January' => 'Januar_1', 'February' => 'Februar_1', 'March' => 'Marz_1'); + $translations_two = array( 'February' => 'Februar_2', 'March' => 'Marz_2', 'April' => 'April_2'); + $translations_three = array('April' => 'April_3', 'May' => 'Mai_3', 'June' => 'Juni_3'); + + // Add a number of files to the local file system to serve as remote + // translation server and match the project definitions set in + // locale_test_locale_translation_projects_alter(). + $this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de._po', $this->timestamp_new, $translations_one); + $this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestamp_old, $translations_two); + $this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de._po', $this->timestamp_old, $translations_three); + + // Add a number of files to the local file system to serve as local + // translation files and match the project definitions set in + // locale_test_locale_translation_projects_alter(). + $this->makePoFile('local', 'contrib_module_one-8.x-1.1.de._po', $this->timestamp_old, $translations_one); + $this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestamp_new, $translations_two); + $this->makePoFile('local', 'contrib_module_three-8.x-1.0.de._po', $this->timestamp_old, $translations_three); + $this->makePoFile('local', 'custom_module_one.de.po', $this->timestamp_new); + } + + /** + * Setup existing translations in the database and set up the status of + * existing translations. + */ + protected function setCurrentTranslations() { + // Add non customized translations to the database. + $langcode = 'de'; + $context = ''; + $non_customized_translations = array( + 'March' => 'Marz', + 'June' => 'Juni', + ); + foreach ($non_customized_translations as $source => $translation) { + $string = $this->container->get('locale.storage')->createString(array( + 'source' => $source, + 'context' => $context, + )) + ->save(); + $this->container->get('locale.storage')->createTranslation(array( + 'lid' => $string->getId(), + 'language' => $langcode, + 'translation' => $translation, + 'customized' => LOCALE_NOT_CUSTOMIZED, + ))->save(); + } + + // Add customized translations to the database. + $customized_translations = array( + 'January' => 'Januar_customized', + 'February' => 'Februar_customized', + 'May' => 'Mai_customized', + ); + foreach ($customized_translations as $source => $translation) { + $string = $this->container->get('locale.storage')->createString(array( + 'source' => $source, + 'context' => $context, + )) + ->save(); + $this->container->get('locale.storage')->createTranslation(array( + 'lid' => $string->getId(), + 'language' => $langcode, + 'translation' => $translation, + 'customized' => LOCALE_CUSTOMIZED, + ))->save(); + } + + // Add a state of current translations in locale_files. + $default = array( + 'langcode' => $langcode, + 'uri' => '', + 'timestamp' => $this->timestamp_medium, + 'last_checked' => $this->timestamp_medium, + ); + $data[] = array( + 'project' => 'contrib_module_one', + 'filename' => 'contrib_module_one-8.x-1.1.de._po', + 'version' => '8.x-1.1', + ); + $data[] = array( + 'project' => 'contrib_module_two', + 'filename' => 'contrib_module_two-8.x-2.0-beta4.de._po', + 'version' => '8.x-2.0-beta4', + ); + $data[] = array( + 'project' => 'contrib_module_three', + 'filename' => 'contrib_module_three-8.x-1.0.de._po', + 'version' => '8.x-1.0', + ); + $data[] = array( + 'project' => 'custom_module_one', + 'filename' => 'custom_module_one.de.po', + 'version' => '', + ); + foreach ($data as $file) { + $file = (object) array_merge($default, $file); + drupal_write_record('locale_file', $file); + } + } + + /** + * Checks the translation of a string. + * + * @param string $source + * Translation source string + * @param string $translation + * Translation to check. Use empty string to check for a not existing + * translation. + * @param string $langcode + * Language code of the language to translate to. + * @param string $message + * (optional) A message to display with the assertion. + */ + protected 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(); + $db_translation = $db_translation == FALSE ? '' : $db_translation; + $this->assertEqual($translation, $db_translation, $message ? $message : format_string('Correct translation of %source (%language)', array('%source' => $source, '%language' => $langcode))); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateCronTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateCronTest.php new file mode 100644 index 0000000..2f7bb3a --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateCronTest.php @@ -0,0 +1,124 @@ + 'Update translations using cron', + 'description' => 'Tests for using cron to update project interface translations.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp(); + $admin_user = $this->drupalCreateUser(array('administer modules', 'administer site configuration', 'administer languages', 'access administration pages', 'translate interface')); + $this->drupalLogin($admin_user); + $this->addLanguage('de'); + } + + /** + * Tests interface translation update using cron. + */ + function testUpdateCron() { + // Set a flag to let the locale_test module replace the project data with a + // set of test projects. + \Drupal::state()->set('locale.test_projects_alter', TRUE); + + // Setup local and remote translations files. + $this->setTranslationFiles(); + config('locale.settings')->set('translation.default_filename', '%project-%version.%language._po')->save(); + + // Update translations using batch to ensure a clean test starting point. + $this->drupalGet('admin/reports/translations/check'); + $this->drupalPost('admin/reports/translations', array(), t('Update translations')); + + // Store translation status for comparison. + $initial_history = locale_translation_get_file_history(); + + // Prepare for test: Simulate new translations being availabe. + // Change the last updated timestamp of a translation file. + $contrib_module_two_uri = 'public://local/contrib_module_two-8.x-2.0-beta4.de._po'; + touch(drupal_realpath($contrib_module_two_uri), REQUEST_TIME); + + // Prepare for test: Simulate that the file has not been checked for a long + // time. Set the last_check timestamp to zero. + $query = db_update('locale_file'); + $query->fields(array('last_checked' => 0)); + $query->condition('project', 'contrib_module_two'); + $query->condition('langcode', 'de'); + $query->execute(); + + // Test: Disable cron update and verify that no tasks are added to the + // queue. + $edit = array( + 'update_interval_days' => 0, + ); + $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); + + // Execute locale cron taks to add tasks to the queue. + locale_cron(); + + // Check whether no tasks are added to the queue. + $queue = \Drupal::queue('locale_translation', TRUE); + $this->assertEqual($queue->numberOfItems(), 0, 'Queue is empty'); + + // Test: Enable cron update and check if update tasks are added to the + // queue. + // Set cron update to Weekly. + $edit = array( + 'update_interval_days' => 7, + ); + $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); + + // Execute locale cron taks to add tasks to the queue. + locale_cron(); + + // Check whether tasks are added to the queue. + $queue = \Drupal::queue('locale_translation', TRUE); + // @todo Make sure 'drupal' project will not show up in list of projects. + // Follow-up issue: https://drupal.org/node/2021749 + $this->assertEqual($queue->numberOfItems(), 6, 'Queue holds tasks for two projects.'); + $item = $queue->claimItem(); + $queue->releaseItem($item); + $this->assertEqual($item->data[1][0], 'contrib_module_two', 'Queue holds tasks for contrib module one.'); + + // Test: Run cron for a second time and check if tasks are not added to + // the queue twice. + locale_cron(); + + // Check whether no more tasks are added to the queue. + $queue = \Drupal::queue('locale_translation', TRUE); + $this->assertEqual($queue->numberOfItems(), 6, 'Queue holds tasks for two projects.'); + + // Test: Execute cron and check if tasks are executed correctly. + // Run cron to process the tasks in the queue. + $this->drupalGet('admin/reports/status/run-cron'); + + drupal_static_reset('locale_translation_get_file_history'); + $history = locale_translation_get_file_history(); + $initial = $initial_history['contrib_module_two']['de']; + $current = $history['contrib_module_two']['de']; + $this->assertTrue($current->timestamp > $initial->timestamp, 'Timestamp is updated'); + $this->assertTrue($current->last_checked > $initial->last_checked, 'Last checked is updated'); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateInterfaceTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateInterfaceTest.php index 3c9d390..0a502b6 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateInterfaceTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateInterfaceTest.php @@ -12,7 +12,7 @@ /** * Tests for the locale translation update status user interfaces. */ -class LocaleUpdateInterfaceTest extends WebTestBase { +class LocaleUpdateInterfaceTest extends LocaleUpdateBase { /** * Modules to enable. @@ -51,16 +51,13 @@ function testInterface() { $this->assertRaw(t('No translatable languages available. Add a language first.', array('@add_language' => url('admin/config/regional/language'))), 'Language message'); // Add German language. - $edit = array( - 'predefined_langcode' => 'de', - ); - $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + $this->addLanguage('de'); // Drupal core is probably in 8.x, but tests may also be executed with // stable releases. As this is an uncontrolled factor in the test, we will - // ignore Drupal core here and continue with the prepared modules. - $status = \Drupal::state()->get('locale.translation_status'); - unset($status['drupal']); + // mark Drupal core as translated and continue with the prepared modules. + $status = locale_translation_get_status(); + $status['drupal']['de']->type = 'current'; \Drupal::state()->set('locale.translation_status', $status); // One language added, all translations up to date. @@ -71,7 +68,7 @@ function testInterface() { $this->assertText(t('All translations up to date.'), 'Translations up to date'); // Set locale_test_translate module to have a local translation available. - $status = \Drupal::state()->get('locale.translation_status'); + $status = locale_translation_get_status(); $status['locale_test_translate']['de']->type = 'local'; \Drupal::state()->set('locale.translation_status', $status); @@ -84,9 +81,9 @@ function testInterface() { // Set locale_test_translate module to have a dev release and no // translation found. - $status = \Drupal::state()->get('locale.translation_status'); + $status = locale_translation_get_status(); $status['locale_test_translate']['de']->version = '1.3-dev'; - unset($status['locale_test_translate']['de']->type); + $status['locale_test_translate']['de']->type = ''; \Drupal::state()->set('locale.translation_status', $status); // Check if no updates were found. diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php index f85be0f..44bfabe 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php @@ -12,35 +12,7 @@ /** * Tests for update translations. */ -class LocaleUpdateTest extends WebTestBase { - - /** - * The path of the translations directory where local translations are stored. - * - * @var string - */ - private $tranlations_directory; - - /** - * Timestamp for an old translation. - * - * @var integer - */ - private $timestamp_old; - - /** - * Timestamp for a medium aged translation. - * - * @var integer - */ - private $timestamp_medium; - - /** - * Timestamp for a new translation. - * - * @var integer - */ - private $timestamp_new; +class LocaleUpdateTest extends LocaleUpdateBase { /** * Modules to enable. @@ -66,249 +38,7 @@ function setUp() { // We use German as test language. This language must match the translation // file that come with the locale_test module (test.de.po) and can therefore // not be chosen randomly. - $this->drupalPost('admin/config/regional/language/add', array('predefined_langcode' => 'de'), t('Add language')); - - // Setup timestamps to identify old and new translation sources. - $this->timestamp_old = REQUEST_TIME - 300; - $this->timestamp_medium = REQUEST_TIME - 200; - $this->timestamp_new = REQUEST_TIME - 100; - $this->timestamp_now = REQUEST_TIME; - } - - /** - * Sets the value of the default translations directory. - * - * @param string $path - * Path of the translations directory relative to the drupal installation - * directory. - */ - private function setTranslationsDirectory($path) { - $this->tranlations_directory = $path; - file_prepare_directory($path, FILE_CREATE_DIRECTORY); - config('locale.settings')->set('translation.path', $path)->save(); - } - - /** - * Adds 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 - * Path of the file relative to the public file path. - * @param string $filename - * Name of the file to create. - * @param integer $timestamp - * Timestamp to set the file to. Defaults to current time. - * @param array $translations - * Array of source/target value translation strings. Only singular strings - * are supported, no plurals. No double quotes are allowed in source and - * translations strings. - */ - private function makePoFile($path, $filename, $timestamp = NULL, $translations = array()) { - $timestamp = $timestamp ? $timestamp : REQUEST_TIME; - $path = 'public://' . $path; - $text = ''; - $po_header = << 1);\\n" - -EOF; - - // Convert array of translations to Gettext source and translation strings. - if ($translations) { - foreach ($translations as $source => $target) { - $text .= 'msgid "'. $source . '"' . "\n"; - $text .= 'msgstr "'. $target . '"' . "\n"; - } - } - - file_prepare_directory($path, FILE_CREATE_DIRECTORY); - $file = entity_create('file', array( - 'uid' => 1, - 'filename' => $filename, - 'uri' => $path . '/' . $filename, - 'filemime' => 'text/x-gettext-translation', - 'timestamp' => $timestamp, - 'status' => FILE_STATUS_PERMANENT, - )); - file_put_contents($file->getFileUri(), $po_header . $text); - touch(drupal_realpath($file->getFileUri()), $timestamp); - $file->save(); - } - - /** - * Setup the environment containting local and remote translation 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. - \Drupal::state()->set('locale.test_projects_alter', TRUE); - - // Setup the environment. - $public_path = variable_get('file_public_path', conf_path() . '/files'); - $this->setTranslationsDirectory($public_path . '/local'); - $config->set('translation.default_filename', '%project-%version.%language._po')->save(); - - // Setting up sets of translations for the translation files. - $translations_one = array('January' => 'Januar_1', 'February' => 'Februar_1', 'March' => 'Marz_1'); - $translations_two = array( 'February' => 'Februar_2', 'March' => 'Marz_2', 'April' => 'April_2'); - $translations_three = array('April' => 'April_3', 'May' => 'Mai_3', 'June' => 'Juni_3'); - - // Add a number of files to the local file system to serve as remote - // translation server and match the project definitions set in - // locale_test_locale_translation_projects_alter(). - $this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de._po', $this->timestamp_new, $translations_one); - $this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestamp_old, $translations_two); - $this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de._po', $this->timestamp_old, $translations_three); - - // Add a number of files to the local file system to serve as local - // translation files and match the project definitions set in - // locale_test_locale_translation_projects_alter(). - $this->makePoFile('local', 'contrib_module_one-8.x-1.1.de._po', $this->timestamp_old, $translations_one); - $this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestamp_new, $translations_two); - $this->makePoFile('local', 'contrib_module_three-8.x-1.0.de._po', $this->timestamp_old, $translations_three); - $this->makePoFile('local', 'custom_module_one.de.po', $this->timestamp_new); - } - - /** - * Setup existing translations in the database and set up the status of - * existing translations. - */ - private function setCurrentTranslations() { - // Add non customized translations to the database. - $langcode = 'de'; - $context = ''; - $non_customized_translations = array( - 'March' => 'Marz', - 'June' => 'Juni', - ); - foreach ($non_customized_translations as $source => $translation) { - $string = $this->container->get('locale.storage')->createString(array( - 'source' => $source, - 'context' => $context, - )) - ->save(); - $target = $this->container->get('locale.storage')->createTranslation(array( - 'lid' => $string->getId(), - 'language' => $langcode, - 'translation' => $translation, - 'customized' => LOCALE_NOT_CUSTOMIZED, - ))->save(); - } - - // Add customized translations to the database. - $customized_translations = array( - 'January' => 'Januar_customized', - 'February' => 'Februar_customized', - 'May' => 'Mai_customized', - ); - foreach ($customized_translations as $source => $translation) { - $string = $this->container->get('locale.storage')->createString(array( - 'source' => $source, - 'context' => $context, - )) - ->save(); - $target = $this->container->get('locale.storage')->createTranslation(array( - 'lid' => $string->getId(), - 'language' => $langcode, - 'translation' => $translation, - 'customized' => LOCALE_CUSTOMIZED, - ))->save(); - } - - // Add a state of current translations in locale_files. - $default = array( - 'langcode' => $langcode, - 'uri' => '', - 'timestamp' => $this->timestamp_medium, - 'last_checked' => $this->timestamp_medium, - ); - $data[] = array( - 'project' => 'contrib_module_one', - 'filename' => 'contrib_module_one-8.x-1.1.de._po', - 'version' => '8.x-1.1', - ); - $data[] = array( - 'project' => 'contrib_module_two', - 'filename' => 'contrib_module_two-8.x-2.0-beta4.de._po', - 'version' => '8.x-2.0-beta4', - ); - $data[] = array( - 'project' => 'contrib_module_three', - 'filename' => 'contrib_module_three-8.x-1.0.de._po', - 'version' => '8.x-1.0', - ); - $data[] = array( - 'project' => 'custom_module_one', - 'filename' => 'custom_module_one.de.po', - 'version' => '', - ); - foreach ($data as $file) { - $file = (object) array_merge($default, $file); - drupal_write_record('locale_file', $file); - } - } - - /** - * Checks the translation of a string. - * - * @param string $source - * Translation source string - * @param string $translation - * Translation to check. Use empty string to check for a not existing - * translation. - * @param string $langcode - * Language code of the language to translate to. - * @param string $message - * (optional) A message to display with the assertion. - */ - 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(); - $db_translation = $db_translation == FALSE ? '' : $db_translation; - $this->assertEqual($translation, $db_translation, $message ? $message : format_string('Correct translation of %source (%language)', array('%source' => $source, '%language' => $langcode))); + $this->addLanguage('de'); } /** @@ -384,7 +114,7 @@ function testUpdateCheckStatus() { // Get status of translation sources at local file system. $this->drupalGet('admin/reports/translations/check'); - $result = \Drupal::state()->get('locale.translation_status'); + $result = locale_translation_get_status(); $this->assertEqual($result['contrib_module_one']['de']->type, LOCALE_TRANSLATION_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, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_two found'); @@ -400,8 +130,8 @@ function testUpdateCheckStatus() { // Get status of translation sources at both local and remote locations. $this->drupalGet('admin/reports/translations/check'); - $result = \Drupal::state()->get('locale.translation_status'); - $this->assertEqual($result['contrib_module_one']['de']->type, 'remote', 'Translation of contrib_module_one found'); + $result = locale_translation_get_status(); + $this->assertEqual($result['contrib_module_one']['de']->type, LOCALE_TRANSLATION_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, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_two found'); $this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestamp_new, 'Translation timestamp found'); @@ -417,7 +147,6 @@ function testUpdateCheckStatus() { * Test conditions: * - Source: remote and local files * - Import overwrite: all existing translations - * - Translation directory: available */ function testUpdateImportSourceRemote() { $config = config('locale.settings'); @@ -440,7 +169,6 @@ function testUpdateImportSourceRemote() { // Check the status on the Available translation status page. $this->assertRaw('', 'German language found'); $this->assertText('Updates for: Contributed module one, Contributed module two, Custom module one, Locale test', 'Updates found'); - $this->assertText('Updates for: Contributed module one, Contributed module two, Custom module one, Locale test', 'Updates found'); $this->assertText('Contributed module one (' . format_date($this->timestamp_now, 'html_date') . ')', 'Updates for Contrib module one'); $this->assertText('Contributed module two (' . format_date($this->timestamp_new, 'html_date') . ')', 'Updates for Contrib module two'); @@ -448,7 +176,7 @@ function testUpdateImportSourceRemote() { $this->drupalPost('admin/reports/translations', array(), t('Update translations')); // Check if the translation has been updated, using the status cache. - $status = \Drupal::state()->get('locale.translation_status'); + $status = locale_translation_get_status(); $this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found'); $this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found'); $this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found'); @@ -481,7 +209,6 @@ function testUpdateImportSourceRemote() { * Test conditions: * - Source: local files only * - Import overwrite: all existing translations - * - Translation directory: available */ function testUpdateImportSourceLocal() { $config = config('locale.settings'); @@ -503,7 +230,7 @@ function testUpdateImportSourceLocal() { $this->drupalPost('admin/reports/translations', array(), t('Update translations')); // Check if the translation has been updated, using the status cache. - $status = \Drupal::state()->get('locale.translation_status'); + $status = locale_translation_get_status(); $this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found'); $this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found'); $this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found'); @@ -531,68 +258,11 @@ function testUpdateImportSourceLocal() { } /** - * Tests translation import without a translations directory. - * - * Test conditions: - * - Source: remote and local files - * - Import overwrite: all existing translations - * - Translation directory: not available - */ - function testUpdateImportWithoutDirectory() { - $config = config('locale.settings'); - - // Build the test environment. - $this->setTranslationFiles(); - $this-> setCurrentTranslations(); - $config->set('translation.default_filename', '%project-%version.%language._po'); - - // Set the update conditions for this test. - $this->setTranslationsDirectory(''); - $edit = array( - 'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL, - 'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL, - ); - $this->drupalPost('admin/config/regional/translate/settings', $edit, t('Save configuration')); - - // Execute the translation update. - $this->drupalGet('admin/reports/translations/check'); - $this->drupalPost('admin/reports/translations', array(), t('Update translations')); - - // Check if the translation has been updated, using the status cache. - $status = \Drupal::state()->get('locale.translation_status'); - $this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found'); - $this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found'); - $this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found'); - - // Check the new translation status. - // 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(); - $this->assertTrue($history['contrib_module_one']['de']->timestamp >= $this->timestamp_now, 'Translation of contrib_module_one is imported'); - $this->assertTrue($history['contrib_module_one']['de']->last_checked >= $this->timestamp_now, 'Translation of contrib_module_one is updated'); - $this->assertEqual($history['contrib_module_two']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_two is imported'); - $this->assertEqual($history['contrib_module_two']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_two is updated'); - $this->assertEqual($history['contrib_module_three']['de']->timestamp, $this->timestamp_medium, 'Translation of contrib_module_three is not imported'); - $this->assertEqual($history['contrib_module_three']['de']->last_checked, $this->timestamp_medium, 'Translation of contrib_module_three is not updated'); - - // Check whether existing translations have (not) been overwritten. - $this->assertEqual(t('January', array(), array('langcode' => 'de')), 'Januar_1', 'Translation of January'); - $this->assertEqual(t('February', array(), array('langcode' => 'de')), 'Februar_1', 'Translation of February'); - $this->assertEqual(t('March', array(), array('langcode' => 'de')), 'Marz_1', 'Translation of March'); - $this->assertEqual(t('May', array(), array('langcode' => 'de')), 'Mai_customized', 'Translation of May'); - $this->assertEqual(t('June', array(), array('langcode' => 'de')), 'Juni', 'Translation of June'); - $this->assertEqual(t('Monday', array(), array('langcode' => 'de')), 'Montag', 'Translation of Monday'); - } - - /** - * Tests translation import with a translations directory and only overwrite - * non-customized translations. + * Tests translation import and only overwrite non-customized translations. * * Test conditions: * - Source: remote and local files * - Import overwrite: only overwrite non-customized translations - * - Translation directory: available */ function testUpdateImportModeNonCustomized() { $config = config('locale.settings'); @@ -624,13 +294,11 @@ function testUpdateImportModeNonCustomized() { } /** - * Tests translation import with a translations directory and don't overwrite - * any translation. + * Tests translation import and don't overwrite any translation. * * Test conditions: * - Source: remote and local files * - Import overwrite: don't overwrite any existing translation - * - Translation directory: available */ function testUpdateImportModeNone() { $config = config('locale.settings'); diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc index 352b0a5..825a332 100644 --- a/core/modules/locale/locale.batch.inc +++ b/core/modules/locale/locale.batch.inc @@ -16,149 +16,72 @@ require_once __DIR__ . '/locale.translation.inc'; /** - * Batch operation callback: Check the availability of a remote po file. + * Batch operation callback: Check status of a remote and local po file. * - * Checks the presence and creation time of one po file per batch process. The - * file URL and timestamp are stored. + * Checks the presence and creation time po translation files in located at + * remote server location and local file system. * - * @param array $source - * A translation source object of the project for which to check the state of - * a remote po file. + * @param string $project + * Machine name of the project for which to check the translation status. + * @param string $langcode + * Language code of the language for which to check the translation. + * @param array $options + * Optional, an array with options that can have the following elements: + * - 'finish_feedback': Whether or not to give feedback to the user when the + * batch is finished. Optional, defaults to TRUE. + * - 'use_remote': Whether or not to check the remote translation file. + * Optional, defaults to TRUE. * @param array $context - * The batch context array. The collected state is stored in the 'results' - * parameter of the context. - * - * @see locale_translation_batch_status_fetch_local() - * @see locale_translation_batch_status_compare() + * The batch context. */ -function locale_translation_batch_status_fetch_remote($source, &$context) { - // Check the translation file at the remote server and update the source - // data with the remote status. - if (isset($source->files[LOCALE_TRANSLATION_REMOTE])) { - $remote_file = $source->files[LOCALE_TRANSLATION_REMOTE]; - $result = locale_translation_http_check($remote_file->uri); +function locale_translation_batch_status_check($project, $langcode, $options = array(), &$context) { + $t = get_t(); + $failure = $checked = FALSE; + $options += array( + 'finish_feedback' => TRUE, + 'use_remote' => TRUE, + ); + $source = locale_translation_get_status(array($project), array($langcode)); + $source = $source[$project][$langcode]; + + // Check the status of local translation files. + if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) { + if ($file = locale_translation_source_check_file($source)) { + locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file); + } + $checked = TRUE; + } - if ($result) { + // Check the status of remote translation files. + if ($options['use_remote'] && isset($source->files[LOCALE_TRANSLATION_REMOTE])) { + $remote_file = $source->files[LOCALE_TRANSLATION_REMOTE]; + if ($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 a file is not found we don't update the - // file object, and store it unchanged. + // store the resulting uri. if (isset($result['last_modified'])) { $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri; $remote_file->timestamp = $result['last_modified']; - $source->files[LOCALE_TRANSLATION_REMOTE] = $remote_file; + locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_REMOTE, $remote_file); } - // Record success. - $context['results']['files'][$source->name] = $source->name; + // @todo What to do with when the file is not found (404)? To prevent + // re-checking within the TTL (1day, 1week) we can set a last_checked + // timestamp or cache the result. + $checked = TRUE; } 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; + $failure = TRUE; } - $context['results']['sources'][$source->name][$source->langcode] = $source; - $context['message'] = t('Checked translation for %project.', array('%project' => $source->project)); } -} - -/** - * Batch operation callback: Check the availability of local po files. - * - * Checks the presence and creation time of po files in the local file system. - * The file path and the timestamp are stored. - * - * @param array $sources - * Array of translation source objects of projects for which to check the - * state of local po files. - * @param array $context - * The batch context array. The collected state is stored in the 'results' - * parameter of the context. - * - * @see locale_translation_batch_status_fetch_remote() - * @see locale_translation_batch_status_compare() - */ -function locale_translation_batch_status_fetch_local($sources, &$context) { - // Get the status of local translation files and store the result data in the - // batch results for later processing. - foreach ($sources as $source) { - if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) { - locale_translation_source_check_file($source); - // If remote data was collected before, we merge it into the newly - // collected result. - if (isset($context['results']['sources'][$source->name][$source->langcode])) { - $source->files[LOCALE_TRANSLATION_REMOTE] = $context['results']['sources'][$source->name][$source->langcode]->files[LOCALE_TRANSLATION_REMOTE]; - } - - // Record success and store the updated source data. - $context['results']['files'][$source->name] = $source->name; - $context['results']['sources'][$source->name][$source->langcode] = $source; - } + // Provide user feedback and record success or failure for reporting at the + // end of the batch. + if ($options['finish_feedback'] && $checked) { + $context['results']['files'][] = $source->name; } - $context['message'] = t('Checked all translations.'); -} - -/** - * Batch operation callback: Compare states and store the result. - * - * In the preceding batch processes data of remote and local translation sources - * is collected. Here we compare the collected results and update the source - * object with the data of the most recent translation file. The end result is - * stored in the 'locale.translation_status' state variable. Other - * processes can collect this data after the batch process is completed. - * - * @param array $context - * The batch context array. The 'results' element contains a structured array - * of project data with languages, local and remote source data. - * - * @see locale_translation_batch_status_fetch_remote() - * @see locale_translation_batch_status_fetch_local() - */ -function locale_translation_batch_status_compare(&$context) { - $history = locale_translation_get_file_history(); - $results = array(); - - if (isset($context['results']['sources'])) { - foreach ($context['results']['sources'] as $project => $langcodes) { - foreach ($langcodes as $langcode => $source) { - $local = isset($source->files[LOCALE_TRANSLATION_LOCAL]) ? $source->files[LOCALE_TRANSLATION_LOCAL] : NULL; - $remote = isset($source->files[LOCALE_TRANSLATION_REMOTE]) ? $source->files[LOCALE_TRANSLATION_REMOTE] : NULL; - - // The available translation files are compared and data of the most - // recent file is used to update the source object. - $file = _locale_translation_source_compare($local, $remote) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $remote : $local; - if (isset($file->timestamp)) { - $source->type = $file->type; - $source->timestamp = $file->timestamp; - } - - // Compare the available translation with the current translations - // status. If the project/language was translated before and it is more - // recent than the most recent translation, the translation is up to - // date. Which is marked in the source object with type "current". - if (isset($history[$source->project][$source->langcode])) { - $current = $history[$source->project][$source->langcode]; - // Add the current translation to the source object to save it in - // the status cache. - $source->files[LOCALE_TRANSLATION_CURRENT] = $current; - - if (isset($source->type)) { - $available = $source->files[$source->type]; - $result = _locale_translation_source_compare($current, $available) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $available : $current; - $source->type = $result->type; - $source->timestamp = $result->timestamp; - } - else { - $source->type = $current->type; - $source->timestamp = $current->timestamp; - } - } - - $results[$project][$langcode] = $source; - } - } - $context['message'] = t('Updated translation status.'); + if ($failure && !$checked) { + $context['results']['failed_files'][] = $source->name; } - locale_translation_status_save($results); + $context['message'] = $t('Checked translation for %project.', array('%project' => $source->project)); } /** @@ -182,7 +105,7 @@ function locale_translation_batch_status_finished($success, $results) { } if (isset($results['files'])) { drupal_set_message(format_plural( - count($results['sources']), + count($results['files']), 'Checked available interface translation updates for one project.', 'Checked available interface translation updates for @count projects.' )); @@ -190,6 +113,7 @@ function locale_translation_batch_status_finished($success, $results) { if (!isset($results['failed_files']) && !isset($results['files'])) { drupal_set_message(t('Nothing to check.')); } + Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME); } else { drupal_set_message(t('An error occurred trying to check available interface translation updates.'), 'error'); @@ -197,63 +121,33 @@ function locale_translation_batch_status_finished($success, $results) { } /** - * Loads translation source data for the projects to be updated. - * - * Source data is loaded from cache and stored in the context results array. - * Source data contains the translations status per project / per language - * and whether translation updates are available and where the updates can be - * retrieved from. The data is stored in the $context['results'] parameter - * so that other batch operations can take this data as input for their - * operation. - * - * @see locale_translation_batch_fetch_download() - * @see locale_translation_batch_fetch_import() - * @see locale_translation_batch_fetch_update_status() - * @see locale_translation_batch_status_compare() - */ -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']); -} - -/** * Batch operation: Download a remote translation file. * - * This operation downloads a remote gettext file and saves it in the temporary - * directory. The remote file URL is taken from the input data in - * $context['results']['input']. The result of the operation is stored in - * $context['results']['sources'] and contains the URL of the temporary file. + * Downloads a remote gettext file into the translations directory. When + * successfully the translation status is updated. * * @param object $project * Source object of the translatable project. * @param string $langcode * Language code. - * @param $context - * Batch context array. + * @param array $context + * The batch context. * - * @see locale_translation_batch_fetch_sources() * @see locale_translation_batch_fetch_import() - * @see locale_translation_batch_fetch_update_status() - * @see locale_translation_batch_status_compare() */ function locale_translation_batch_fetch_download($project, $langcode, &$context) { - $sources = $context['results']['input']; - if (isset($sources[$project . ':' . $langcode])) { - $source = $sources[$project . ':' . $langcode]; + $sources = locale_translation_get_status(array($project), array($langcode)); + if (isset($sources[$project][$langcode])) { + $source = $sources[$project][$langcode]; if (isset($source->type) && $source->type == LOCALE_TRANSLATION_REMOTE) { - if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE])) { - $context['message'] = t('Downloaded translation for %project.', array('%project' => $source->project)); - $source->files[LOCALE_TRANSLATION_DOWNLOADED] = $file; + $t = get_t(); + if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE], 'translations://')) { + $context['message'] = $t('Downloaded translation for %project.', array('%project' => $source->project)); + locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file); } else { $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE]; } - $context['results']['sources'][$project][$langcode] = $source; } } } @@ -261,12 +155,8 @@ function locale_translation_batch_fetch_download($project, $langcode, &$context) /** * Batch process: Import translation file. * - * This batch operation imports either a local gettext file or a downloaded - * remote gettext file. In case of a downloaded file the location of the - * temporary file is found in the $context['results']['sources']. The temporary - * file will be deleted after importing or will be moved to the local - * translations directory. In case of a local file the file will just be - * imported. + * Imports a gettext file from the translation directory. When successfully the + * translation status is updated. * * @param object $project * Source object of the translatable project. @@ -274,42 +164,26 @@ function locale_translation_batch_fetch_download($project, $langcode, &$context) * Language code. * @param array $options * Array of import options. - * @param $context - * Batch context array. + * @param array $context + * The batch context. * * @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() - * @see locale_translation_batch_status_compare() */ function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) { - $sources = $context['results']['input']; - if (isset($sources[$project . ':' . $langcode])) { - $source = $sources[$project . ':' . $langcode]; + $sources = locale_translation_get_status(array($project), array($langcode)); + if (isset($sources[$project][$langcode])) { + $source = $sources[$project][$langcode]; if (isset($source->type)) { if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) { - - // If we are working on a remote file we will import the downloaded - // file. If the file was local just mark the result as such. - if ($source->type == LOCALE_TRANSLATION_REMOTE) { - if (isset($context['results']['sources'][$source->project][$source->langcode]->files[LOCALE_TRANSLATION_DOWNLOADED])) { - $import_type = LOCALE_TRANSLATION_DOWNLOADED; - $source_result = $context['results']['sources'][$source->project][$source->langcode]; - } - } - else { - $import_type = LOCALE_TRANSLATION_LOCAL; - $source_result = $source; - } - - $file = $source_result->files[$import_type]; + $t = get_t(); + $file = $source->files[LOCALE_TRANSLATION_LOCAL]; module_load_include('bulk.inc', 'locale'); $options += array( 'message' => t('Importing translation for %project.', array('%project' => $source->project)), ); // Import the translation file. For large files the batch operations is - // progressive and will be called repeatedly untill finished. + // progressive and will be called repeatedly until finished. locale_translate_batch_import($file, $options, $context); // The import is finished. @@ -318,96 +192,13 @@ function locale_translation_batch_fetch_import($project, $langcode, $options, &$ if (isset($context['results']['files'][$file->uri])) { $context['message'] = t('Imported translation for %project.', array('%project' => $source->project)); - // Keep the data of imported source. In the following batch - // operation it will be saved in the {locale_file} table. - $source_result->files[LOCALE_TRANSLATION_IMPORTED] = $source_result->files[$source->type]; - - // Downloaded files are stored in the temporary files directory. If - // files should be kept locally, they will be moved to the local - // translations after successfull import. Otherwise the temporary - // file is deleted after being imported. - if ($import_type == LOCALE_TRANSLATION_DOWNLOADED && config('locale.settings')->get('translation.path') && isset($source_result->files[LOCALE_TRANSLATION_LOCAL])) { - if (file_unmanaged_move($file->uri, $source_result->files[LOCALE_TRANSLATION_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 - // local file. - $import_type = LOCALE_TRANSLATION_LOCAL; - } - } - // The downloaded file is imported but will not be stored locally. - // Store the timestamp and delete the file. - if ($import_type == LOCALE_TRANSLATION_DOWNLOADED) { - $timestamp = filemtime($source_result->files[$import_type]->uri); - $source_result->files[LOCALE_TRANSLATION_IMPORTED]->timestamp = $timestamp; - $source_result->files[LOCALE_TRANSLATION_IMPORTED]->last_checked = REQUEST_TIME; - file_unmanaged_delete($file->uri); - } - // If the translation file is stored in the local directory. The - // timestamp of the file is stored. - if ($import_type == LOCALE_TRANSLATION_LOCAL) { - $timestamp = filemtime($source_result->files[$import_type]->uri); - $source_result->files[LOCALE_TRANSLATION_LOCAL]->timestamp = $timestamp; - $source_result->files[LOCALE_TRANSLATION_IMPORTED]->timestamp = $timestamp; - $source_result->files[LOCALE_TRANSLATION_IMPORTED]->last_checked = REQUEST_TIME; - - } + // Save the data of imported source into the {locale_file} table and + // update the current translation status. + locale_translation_status_save($project, $langcode, LOCALE_TRANSLATION_CURRENT, $source->files[LOCALE_TRANSLATION_LOCAL]); } - else { - // File import failed. We can delete the temporary file. - if ($import_type == LOCALE_TRANSLATION_DOWNLOADED) { - file_unmanaged_delete($file->uri); - } - } - } - $context['results']['sources'][$source->project][$source->langcode] = $source_result; - } - } - } -} - -/** - * Batch process: Update the download history table. - * - * This batch process updates the {local_file} table with the data of imported - * gettext files. Import data is taken from $context['results']['sources']. - * - * @param $context - * Batch context array. - * - * @see locale_translation_batch_fetch_sources() - * @see locale_translation_batch_fetch_download() - * @see locale_translation_batch_fetch_import() - * @see locale_translation_batch_status_compare() - */ -function locale_translation_batch_fetch_update_status(&$context) { - $results = array(); - - if (isset($context['results']['sources'])) { - foreach ($context['results']['sources'] as $project => $langcodes) { - foreach ($langcodes as $langcode => $source) { - - // Store the state of the imported translations in {locale_file} table. - // During the batch execution the data of the imported files is - // temporary stored in $context['results']['sources']. Now it will be - // stored in the database. Afterwards the temporary import and download - // data can be deleted. - if (isset($source->files[LOCALE_TRANSLATION_IMPORTED])) { - $file = $source->files[LOCALE_TRANSLATION_IMPORTED]; - locale_translation_update_file_history($file); - unset($source->files[LOCALE_TRANSLATION_IMPORTED]); } - unset($source->files[LOCALE_TRANSLATION_DOWNLOADED]); - - // 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 used to update the translation status. - $context['results']['sources'][$project][$langcode] = $source; } } - $context['message'] = t('Updated translations.'); - - // The file history has changed, flush the static cache now. - drupal_static_reset('locale_translation_get_file_history'); } } @@ -421,6 +212,9 @@ function locale_translation_batch_fetch_update_status(&$context) { */ function locale_translation_batch_fetch_finished($success, $results) { module_load_include('bulk.inc', 'locale'); + if ($success) { + Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME); + } return locale_translate_batch_finished($success, $results); } @@ -478,9 +272,7 @@ function locale_translation_http_check($uri) { } /** - * Downloads source file from a remote server. - * - * The downloaded file is stored in the temporary files directory. + * Downloads a translation file from a remote server. * * @param object $source_file * Source file object with at least: @@ -489,21 +281,22 @@ function locale_translation_http_check($uri) { * - "langcode": Translation language. * - "version": Project version. * - "filename": File name. + * @param string $directory + * Directory where the downloaded file will be saved. Defaults to the + * temporary file path. * * @return object * 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://')) { - $file = new stdClass(); - $file->project = $source_file->project; - $file->langcode = $source_file->langcode; - $file->version = $source_file->version; - $file->type = LOCALE_TRANSLATION_DOWNLOADED; +function locale_translation_download_source($source_file, $directory = 'temporary://') { + if ($uri = system_retrieve_file($source_file->uri, $directory)) { + $file = clone($source_file); + $file->type = LOCALE_TRANSLATION_LOCAL; $file->uri = $uri; - $file->filename = $source_file->filename; + $file->directory = $directory; + $file->timestamp = filemtime($uri); return $file; } - watchdog('locale', 'Unable to download translation file @uri.', array('@uri' => $source->files[LOCALE_TRANSLATION_REMOTE]->uri), WATCHDOG_ERROR); + watchdog('locale', 'Unable to download translation file @uri.', array('@uri' => $source_file->uri), WATCHDOG_ERROR); return FALSE; } diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index 2fbbaaf..a669025 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -212,31 +212,6 @@ function locale_translation_default_translation_server() { } /** - * Build path to translation source, out of a server path replacement pattern. - * - * @param stdClass $project - * Project object containing data to be inserted in the template. - * @param string $template - * String containing placeholders. Available placeholders: - * - "%project": Project name. - * - "%version": Project version. - * - "%core": Project core version. - * - "%language": Language code. - * - * @return string - * String with replaced placeholders. - */ -function locale_translation_build_server_pattern($project, $template) { - $variables = array( - '%project' => $project->name, - '%version' => $project->version, - '%core' => $project->core, - '%language' => isset($project->langcode) ? $project->langcode : '%language', - ); - return strtr($template, $variables); -} - -/** * Check for the latest release of project translations. * * @param array $projects @@ -247,6 +222,7 @@ function locale_translation_build_server_pattern($project, $template) { * @return array * Available sources indexed by project and language. */ +// @todo Return batch or NULL function locale_translation_check_projects($projects = array(), $langcodes = array()) { if (locale_translation_use_remote_source()) { // Retrieve the status of both remote and local translation sources by @@ -256,6 +232,7 @@ function locale_translation_check_projects($projects = array(), $langcodes = arr else { // Retrieve and save the status of local translations only. locale_translation_check_projects_local($projects, $langcodes); + Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME); } } @@ -281,7 +258,7 @@ function locale_translation_check_projects_batch($projects = array(), $langcodes /** * Builds a batch to get the status of remote and local translation files. * - * The batch process fetches the state of both remote and (if configured) local + * The batch process fetches the state of both local and (if configured) remote * 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 @@ -299,8 +276,9 @@ function locale_translation_check_projects_batch($projects = array(), $langcodes function locale_translation_batch_status_build($projects = array(), $langcodes = array()) { $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $options = _locale_translation_default_update_options(); - $operations = _locale_translation_batch_status_operations($projects, $langcodes); + $operations = _locale_translation_batch_status_operations($projects, $langcodes, $options); $batch = array( 'operations' => $operations, @@ -317,30 +295,26 @@ function locale_translation_batch_status_build($projects = array(), $langcodes = * Helper function to construct batch operations checking remote translation * status. * - * @param array projects + * @param array $projects * Array of project names to be processed. - * @param array langcodes + * @param array $langcodes * Array of language codes. + * @param array $options + * Batch processing options. * * @return array * Array of batch operations. */ -function _locale_translation_batch_status_operations($projects, $langcodes) { +function _locale_translation_batch_status_operations($projects, $langcodes, $options = array()) { $operations = array(); - // Set the batch processes for remote sources. - $sources = locale_translation_build_sources($projects, $langcodes); - if (locale_translation_use_remote_source()) { - foreach ($sources as $source) { - $operations[] = array('locale_translation_batch_status_fetch_remote', array($source)); + foreach ($projects as $project) { + foreach ($langcodes as $langcode) { + // Check status of local and remote translation sources. + $operations[] = array('locale_translation_batch_status_check', array($project, $langcode, $options)); } } - // Check for local sources, compare the results of local and remote and store - // the most recent. - $operations[] = array('locale_translation_batch_status_fetch_local', array($sources)); - $operations[] = array('locale_translation_batch_status_compare', array()); - return $operations; } @@ -348,8 +322,7 @@ function _locale_translation_batch_status_operations($projects, $langcodes) { * Check and store the status and timestamp of local po files. * * 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'. + * files will be ignored. * * 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 @@ -367,8 +340,6 @@ function _locale_translation_batch_status_operations($projects, $langcodes) { function locale_translation_check_projects_local($projects = array(), $langcodes = array()) { $projects = locale_translation_get_projects($projects); $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); - $history = locale_translation_get_file_history(); - $results = array(); // For each project and each language we check if a local po file is // available. When found the source object is updated with the appropriate @@ -376,35 +347,8 @@ function locale_translation_check_projects_local($projects = array(), $langcodes foreach ($projects as $name => $project) { foreach ($langcodes as $langcode) { $source = locale_translation_source_build($project, $langcode); - if (locale_translation_source_check_file($source)) { - $source->type = 'local'; - $source->timestamp = $source->files['local']->timestamp; - } - - // Compare the available translation with the current translations status. - // If the project/language was translated before and it is more recent - // than the most recent translation, the translation is up to date. Which - // is marked in the source object with type "current". - if (isset($history[$source->project][$source->langcode])) { - $current = $history[$source->project][$source->langcode]; - // Add the current translation to the source object to save it in - // the status cache. - $source->files[LOCALE_TRANSLATION_CURRENT] = $current; - - if (isset($source->type)) { - $available = $source->files[$source->type]; - $result = _locale_translation_source_compare($current, $available) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $available : $current; - $source->type = $result->type; - $source->timestamp = $result->timestamp; - } - else { - $source->type = $current->type; - $source->timestamp = $current->timestamp; - } - } - - $results[$name][$langcode] = $source; + $file = locale_translation_source_check_file($source); + locale_translation_status_save($name, $langcode, LOCALE_TRANSLATION_LOCAL, $file); } } - locale_translation_status_save($results); } diff --git a/core/modules/locale/locale.fetch.inc b/core/modules/locale/locale.fetch.inc index da55056..9568691 100644 --- a/core/modules/locale/locale.fetch.inc +++ b/core/modules/locale/locale.fetch.inc @@ -30,8 +30,12 @@ function locale_translation_batch_update_build($projects = array(), $langcodes = module_load_include('compare.inc', 'locale'); $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $status_options = $options; + $status_options['finish_feedback'] = FALSE; - $operations = _locale_translation_batch_status_operations($projects, $langcodes); + // Check status of local and remote translation files. + $operations = _locale_translation_batch_status_operations($projects, $langcodes, $status_options); + // Download and import translations. $operations = array_merge($operations, _locale_translation_fetch_operations($projects, $langcodes, $options)); $batch = array( @@ -90,9 +94,7 @@ function locale_translation_batch_fetch_build($projects = array(), $langcodes = */ function _locale_translation_fetch_operations($projects, $langcodes, $options) { $operations = array(); - $config = config('locale.settings'); - $operations[] = array('locale_translation_batch_fetch_sources', array($projects, $langcodes)); foreach ($projects as $project) { foreach ($langcodes as $langcode) { if (locale_translation_use_remote_source()) { @@ -102,12 +104,5 @@ function _locale_translation_fetch_operations($projects, $langcodes, $options) { } } - // Update and save the translation status. - $operations[] = array('locale_translation_batch_fetch_update_status', array()); - - // Update and save the source status. New translation files have been - // downloaded, so other sources will be newer. We update the status now. - $operations[] = array('locale_translation_batch_status_compare', array()); - return $operations; } diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index 92b28ea..eac6fa4 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -292,17 +292,17 @@ function locale_requirements($phase) { $requirements = array(); if ($phase == 'runtime') { $available_updates = array(); - $updates_not_found = array(); + $untranslated = array(); $languages = locale_translatable_language_list(); if ($languages) { // Determine the status of the translation updates per lanuage. - $status = Drupal::state()->get('locale.translation_status'); + $status = locale_translation_get_status(); if ($status) { foreach ($status as $project_id => $project) { foreach ($project as $langcode => $project_info) { - if (!isset($project_info->type)) { - $updates_not_found[$langcode] = $languages[$langcode]->name; + if (empty($project_info->type)) { + $untranslated[$langcode] = $languages[$langcode]->name; } elseif ($project_info->type == LOCALE_TRANSLATION_LOCAL || $project_info->type == LOCALE_TRANSLATION_REMOTE) { $available_updates[$langcode] = $languages[$langcode]->name; @@ -310,7 +310,7 @@ function locale_requirements($phase) { } } - if ($available_updates || $updates_not_found) { + if ($available_updates || $untranslated) { if ($available_updates) { $requirements['locale_translation'] = array( 'title' => 'Translation update status', @@ -324,7 +324,7 @@ function locale_requirements($phase) { 'title' => 'Translation update status', 'value' => t('Missing translations'), 'severity' => REQUIREMENT_INFO, - 'description' => t('Missing translations for: @languages. See the Available translation updates page for more information.', array('@languages' => implode(', ', $updates_not_found), '@updates' => url('admin/reports/translations'))), + 'description' => t('Missing translations for: @languages. See the Available translation updates page for more information.', array('@languages' => implode(', ', $untranslated), '@updates' => url('admin/reports/translations'))), ); } } diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index dcca8bf..0c6ecb6 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -127,16 +127,6 @@ const LOCALE_TRANSLATION_CURRENT = 'current'; /** - * Translation source is a downloaded file. - */ -const LOCALE_TRANSLATION_DOWNLOADED = 'download'; - -/** - * Translation source is an imported file. - */ -const LOCALE_TRANSLATION_IMPORTED = 'import'; - -/** * Implements hook_help(). */ function locale_help($path, $arg) { @@ -433,6 +423,87 @@ function locale_themes_disabled($themes) { } /** + * Implements hook_cron(). + */ +function locale_cron() { + // Update translations only when an update frequency was set by the admin + // and a translatable language was set. + if ($frequency = config('locale.settings')->get('translation.update_interval_days') && locale_translatable_language_list()) { + module_load_include('translation.inc', 'locale'); + locale_cron_fill_queue(); + } +} + +/** + * Implements hook_queue_info(). + */ +function locale_queue_info() { + $queues['locale_translation'] = array( + 'title' => t('Update translations'), + 'worker callback' => 'locale_translation_worker', + 'cron' => array( + 'time' => 30, + ), + ); + return $queues; +} + +/** + * Callback: Executes interface translation queue tasks. + * + * The translation update functions executed here are batch operations which + * are also used in translation update batches. The batch functions may need to + * be executed multiple times to complete their task, typically this is the + * translation import function. When a batch function is not finished, a new + * queue task is created and added to the end of the queue. The batch context + * data is needed to continue the batch task is stored in the queue with the + * queue data. + * + * @param array $data + * Queue data array containing: + * - Function name. + * - Array of function arguments. Optionally contains the batch context data. + * + * @see locale_queue_info() + */ +function locale_translation_worker($data) { + module_load_include('batch.inc', 'locale'); + list($function, $args) = $data; + + // We execute batch operation functions here to check, download and import the + // translation files. Batch functions use a context variable as last argument + // which is passed by reference. When a batch operation is called for the + // first time a default batch context is created. When called iterative + // (usually the batch import function) the batch context is passed through via + // the queue and is part of the $data. + $last = count($args) - 1; + if (!is_array($args[$last]) || !isset($args[$last]['finished'])) { + $batch_context = array( + 'sandbox' => array(), + 'results' => array(), + 'finished' => 1, + 'message' => '', + ); + } + else { + $batch_context = $args[$last]; + unset ($args[$last]); + } + $args = array_merge($args, array(&$batch_context)); + + // Call the batch operation function. + call_user_func_array($function, $args); + + // If the batch operation is not finished we create a new queue task to + // continue the task. This is typically the translation import task. + if ($batch_context['finished'] < 1) { + unset($batch_context['strings']); + $queue = Drupal::queue('locale_translation', TRUE); + $queue->createItem(array($function, $args)); + } +} + +/** * Imports translations when new modules or themes are installed. * * This function will start a batch to import translations for the added @@ -443,7 +514,6 @@ function locale_themes_disabled($themes) { * translations for, indexed by type. */ function locale_system_update(array $components) { - $components += array('module' => array(), 'theme' => array()); $list = array_merge($components['module'], $components['theme']); @@ -767,6 +837,7 @@ function locale_form_system_file_system_settings_alter(&$form, $form_state) { '#default_value' => config('locale.settings')->get('translation.path'), '#maxlength' => 255, '#description' => t('A local file system path where interface translation files will be stored.'), + '#required' => TRUE, '#after_build' => array('system_check_directory'), '#weight' => 10, ); @@ -829,7 +900,7 @@ function locale_translation_get_file_history() { // Get file history from the database. $result = db_query('SELECT project, langcode, filename, version, uri, timestamp, last_checked FROM {locale_file}'); foreach ($result as $file) { - $file->type = LOCALE_TRANSLATION_CURRENT; + $file->type = $file->timestamp ? LOCALE_TRANSLATION_CURRENT : ''; $history[$file->project][$file->langcode] = $file; } } @@ -855,7 +926,11 @@ function locale_translation_update_file_history($file) { else { $update = array(); } - return drupal_write_record('locale_file', $file, $update); + $result = drupal_write_record('locale_file', $file, $update); + // The file history has changed, flush the static cache now. + // @todo Can we make this more fine grained? + drupal_static_reset('locale_translation_get_file_history'); + return $result; } /** @@ -879,27 +954,92 @@ function locale_translation_file_history_delete($projects = array(), $langcodes } /** + * Gets the current translation status. + * + * @todo What is 'translation status'? + */ +function locale_translation_get_status($projects = NULL, $langcodes = NULL) { + $result = array(); + $status = Drupal::state()->get('locale.translation_status'); + module_load_include('translation.inc', 'locale'); + $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); + $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + + // Get the translation status of each project-language combination. If no + // status was stored, a new translation source is created. + foreach ($projects as $project) { + foreach ($langcodes as $langcode) { + if (isset($status[$project][$langcode])) { + $result[$project][$langcode] = $status[$project][$langcode]; + } + else { + $sources = locale_translation_build_sources(array($project), array($langcode)); + if (isset($sources[$project][$langcode])) { + $result[$project][$langcode] = $sources[$project][$langcode]; + } + } + } + } + return $result; +} + +/** * Saves the status of translation sources in static cache. * + * @param string $project + * Machine readable project name. + * @param string $langcode + * Language code. + * @param string $type + * Type of data to be stored. * @param array $data - * Array of translation source data, structured by project name and langcode. + * File object also containing timestamp when the translation is last updated. */ -function locale_translation_status_save($data) { +function locale_translation_status_save($project, $langcode, $type, $data) { // Followup issue: http://drupal.org/node/1842362 // Split status storage per module/language and expire individually. This will // improve performance for large sites. - $status = Drupal::state()->get('locale.translation_status'); - $status = empty($status) ? array() : $status; - // Merge the new data into the existing structured status array. - foreach ($data as $project => $languages) { - foreach ($languages as $langcode => $source) { - $status[$project][$langcode] = $source; + // Load the translation status or build it if not already available. + module_load_include('translation.inc', 'locale'); + $status = locale_translation_get_status(); + if (empty($status)) { + $projects = locale_translation_get_projects(array($project)); + if (isset($projects[$project])) { + $status[$project][$langcode] = locale_translation_source_build($projects[$project], $langcode); } } - Drupal::state()->set('locale.translation_status', $status); - Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME); + // Merge the new status data with the existing status. + if (isset($status[$project][$langcode])) { + switch ($type) { + case LOCALE_TRANSLATION_REMOTE: + case LOCALE_TRANSLATION_LOCAL: + // Add the source data to the status array. + $status[$project][$langcode]->files[$type] = $data; + + // Check if this translation is the most recent one. Set timestamp and + // data type of the most recent translation source. + if (isset($data->timestamp) && $data->timestamp) { + if ($data->timestamp > $status[$project][$langcode]->timestamp) { + $status[$project][$langcode]->timestamp = $data->timestamp; + $status[$project][$langcode]->last_checked = REQUEST_TIME; + $status[$project][$langcode]->type = $type; + } + } + break; + case LOCALE_TRANSLATION_CURRENT: + $data->last_checked = REQUEST_TIME; + $status[$project][$langcode]->timestamp = $data->timestamp; + $status[$project][$langcode]->last_checked = $data->last_checked; + $status[$project][$langcode]->type = $type; + locale_translation_update_file_history($data); + break; + } + + Drupal::state()->set('locale.translation_status', $status); + Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME); + } } /** @@ -909,7 +1049,7 @@ function locale_translation_status_save($data) { * Language code(s) to be deleted from the cache. */ function locale_translation_status_delete_languages($langcodes) { - if ($status = Drupal::state()->get('locale.translation_status')) { + if ($status = locale_translation_get_status()) { foreach ($status as $project => $languages) { foreach ($languages as $langcode => $source) { if (in_array($langcode, $langcodes)) { @@ -928,7 +1068,7 @@ function locale_translation_status_delete_languages($langcodes) { * Project name(s) to be deleted from the cache. */ function locale_translation_status_delete_projects($projects) { - $status = Drupal::state()->get('locale.translation_status'); + $status = locale_translation_get_status(); foreach ($status as $project => $languages) { if (in_array($project, $projects)) { diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 47ad131..522d776 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -494,14 +494,14 @@ function locale_translation_status_form($form, &$form_state) { module_load_include('compare.inc', 'locale'); $updates = $options = array(); $languages_update = $languages_not_found = array(); + $projects_update = array(); // @todo Calling locale_translation_build_projects() is an expensive way to // get a module name. In follow-up issue http://drupal.org/node/1842362 // the project name will be stored to display use, like here. $project_data = locale_translation_build_projects(); $languages = locale_translatable_language_list(); - $projects = locale_translation_get_projects(); - $status = Drupal::state()->get('locale.translation_status'); + $status = locale_translation_get_status(); // Prepare information about projects which have available translation // updates. @@ -509,7 +509,7 @@ function locale_translation_status_form($form, &$form_state) { foreach ($status as $project_id => $project) { foreach ($project as $langcode => $project_info) { // No translation file found for this project-language combination. - if (!isset($project_info->type)) { + if (empty($project_info->type)) { $updates[$langcode]['not_found'][] = array( 'name' => $project_info->name == 'drupal' ? t('Drupal core') : $project_data[$project_info->name]->info['name'], 'version' => $project_info->version, @@ -528,6 +528,7 @@ function locale_translation_status_form($form, &$form_state) { 'timestamp' => $recent->timestamp, ); $languages_update[$langcode] = $langcode; + $projects_update[$project_info->name] = $project_info->name; } } } @@ -571,6 +572,12 @@ function locale_translation_status_form($form, &$form_state) { $empty = t('No translation status available. Check manually.', array('@check' => url('admin/reports/translations/check'))); } + // The projects which require an update. Used by the _submit callback. + $form['projects_update'] = array( + '#type' => 'value', + '#value' => $projects_update, + ); + $form['langcodes'] = array( '#type' => 'tableselect', '#header' => $header, @@ -614,6 +621,7 @@ function locale_translation_status_form_validate($form, &$form_state) { function locale_translation_status_form_submit($form, &$form_state) { module_load_include('fetch.inc', 'locale'); $langcodes = array_filter($form_state['values']['langcodes']); + $projects = array_filter($form_state['values']['projects_update']); // Set the translation import options. This determines if existing // translations will be overwritten by imported strings. @@ -629,7 +637,7 @@ function locale_translation_status_form_submit($form, &$form_state) { batch_set($batch); } else { - $batch = locale_translation_batch_fetch_build(array(), $langcodes, $options); + $batch = locale_translation_batch_fetch_build($projects, $langcodes, $options); batch_set($batch); } } diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc index 99a8559..3c77ff0 100644 --- a/core/modules/locale/locale.translation.inc +++ b/core/modules/locale/locale.translation.inc @@ -104,12 +104,12 @@ function locale_translation_load_sources($projects = NULL, $langcodes = NULL) { $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); // Load source data from locale_translation_status cache. - $status = Drupal::state()->get('locale.translation_status'); + $status = locale_translation_get_status(); // Use only the selected projects and languages for update. foreach($projects as $project) { foreach ($langcodes as $langcode) { - $sources[$project . ':' . $langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL; + $sources[$project][$langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL; } } return $sources; @@ -124,7 +124,7 @@ function locale_translation_load_sources($projects = NULL, $langcodes = NULL) { * Array of language codes. Defaults to all translatable languages. * * @return array - * Array of source objects. Keyed with :. + * Array of source objects. Keyed by project name and language code. * * @see locale_translation_source_build() */ @@ -136,7 +136,7 @@ function locale_translation_build_sources($projects = array(), $langcodes = arra foreach ($projects as $project) { foreach ($langcodes as $langcode) { $source = locale_translation_source_build($project, $langcode); - $sources[$source->name . ':' . $source->langcode] = $source; + $sources[$source->name][$source->langcode] = $source; } } return $sources; @@ -159,32 +159,24 @@ function locale_translation_build_sources($projects = array(), $langcodes = arra * Translation source object. * * @return stdClass - * File object (filename, basename, name) updated with data of the po file. - * On success the files property of the source object is updated. - * files[LOCALE_TRANSLATION_LOCAL]: + * Source file object of the po file, updated with: * - "uri": File name and path. * - "timestamp": Last updated time of the po file. * FALSE if the file is not found. * * @see locale_translation_source_build() */ -function locale_translation_source_check_file(&$source) { +function locale_translation_source_check_file($source) { if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) { - $directory = $source->files[LOCALE_TRANSLATION_LOCAL]->directory; - $filename = '/' . preg_quote($source->files[LOCALE_TRANSLATION_LOCAL]->filename) . '$/'; - - // If the directory contains a stream wrapper, it is converted to a real - // path. This is required for file_scan_directory() which can not handle - // stream wrappers. - if ($scheme = file_uri_scheme($directory)) { - $directory = str_replace($scheme . '://', drupal_realpath($scheme . '://'), $directory); - } + $source_file = $source->files[LOCALE_TRANSLATION_LOCAL]; + $directory = $source_file->directory; + $filename = '/' . preg_quote($source_file->filename) . '$/'; if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) { $file = current($files); - $source->files[LOCALE_TRANSLATION_LOCAL]->uri = $file->uri; - $source->files[LOCALE_TRANSLATION_LOCAL]->timestamp = filemtime($file->uri); - return $file; + $source_file->uri = $file->uri; + $source_file->timestamp = filemtime($file->uri); + return $source_file; } } return FALSE; @@ -211,13 +203,14 @@ function locale_translation_source_check_file(&$source) { * - "files": Array of file objects containing properties of local and remote * translation files. * Other processes can add the following properties: - * - "type": Most recent file type LOCALE_TRANSLATION_REMOTE or - * LOCALE_TRANSLATION_LOCAL. Corresponding with a key of the - * "files" array. - * - "timestamp": Timestamp of the most recent translation file. + * - "type": Most recent translation source found. LOCALE_TRANSLATION_REMOTE and + * LOCALE_TRANSLATION_LOCAL indicate available new translations, + * LOCALE_TRANSLATION_CURRENT indicate that the current translation is them + * most recent. "type" sorresponds with a key of the "files" array. + * - "timestamp": The creation time of the "type" translation (file). + * - "last_checked": The time when the "type" translation was last checked. * The "files" array can hold file objects of type: - * LOCALE_TRANSLATION_LOCAL, LOCALE_TRANSLATION_REMOTE, - * LOCALE_TRANSLATION_DOWNLOADED, LOCALE_TRANSLATION_IMPORTED and + * LOCALE_TRANSLATION_LOCAL, LOCALE_TRANSLATION_REMOTE and * LOCALE_TRANSLATION_CURRENT. Each contains following properties: * - "type": The object type (LOCALE_TRANSLATION_LOCAL, * LOCALE_TRANSLATION_REMOTE, etc. see above). @@ -239,6 +232,9 @@ function locale_translation_source_build($project, $langcode, $filename = NULL) $source = clone $project; $source->project = $project->name; $source->langcode = $langcode; + $source->type = ''; + $source->timestamp = 0; + $source->last_checked = 0; $filename = $filename ? $filename : config('locale.settings')->get('translation.default_filename'); @@ -256,17 +252,15 @@ function locale_translation_source_build($project, $langcode, $filename = NULL) 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)), 'uri' => locale_translation_build_server_pattern($source, $source->server_pattern), ); - if (config('locale.settings')->get('translation.path')) { - $files[LOCALE_TRANSLATION_LOCAL] = (object) array( - 'project' => $project->name, - 'langcode' => $langcode, - 'version' => $project->version, - 'type' => LOCALE_TRANSLATION_LOCAL, - 'filename' => locale_translation_build_server_pattern($source, $filename), - 'directory' => 'translations://', - ); - $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . $files[LOCALE_TRANSLATION_LOCAL]->filename; - } + $files[LOCALE_TRANSLATION_LOCAL] = (object) array( + 'project' => $project->name, + 'langcode' => $langcode, + 'version' => $project->version, + 'type' => LOCALE_TRANSLATION_LOCAL, + 'filename' => locale_translation_build_server_pattern($source, $filename), + 'directory' => 'translations://', + ); + $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . $files[LOCALE_TRANSLATION_LOCAL]->filename; } else { $files[LOCALE_TRANSLATION_LOCAL] = (object) array( @@ -281,10 +275,94 @@ function locale_translation_source_build($project, $langcode, $filename = NULL) } $source->files = $files; + // If this project+language is already translated, we add its status and + // update the current translation timestamp and last_updated time. If the + // project+language is not translated before, create a new record. + $history = locale_translation_get_file_history(); + if (isset($history[$project->name][$langcode]) && $history[$project->name][$langcode]->timestamp) { + $source->files[LOCALE_TRANSLATION_CURRENT] = $history[$project->name][$langcode]; + $source->type = LOCALE_TRANSLATION_CURRENT; + $source->timestamp = $history[$project->name][$langcode]->timestamp; + $source->last_checked = $history[$project->name][$langcode]->last_checked; + } + else { + locale_translation_update_file_history($source); + } + return $source; } /** + * Build path to translation source, out of a server path replacement pattern. + * + * @param stdClass $project + * Project object containing data to be inserted in the template. + * @param string $template + * String containing placeholders. Available placeholders: + * - "%project": Project name. + * - "%version": Project version. + * - "%core": Project core version. + * - "%language": Language code. + * + * @return string + * String with replaced placeholders. + */ +function locale_translation_build_server_pattern($project, $template) { + $variables = array( + '%project' => $project->name, + '%version' => $project->version, + '%core' => $project->core, + '%language' => isset($project->langcode) ? $project->langcode : '%language', + ); + return strtr($template, $variables); +} + +/** + * Populate a queue with project to check for translation updates. + */ +function locale_cron_fill_queue() { + $updates = array(); + $config = config('locale.settings'); + + // Determine which project+language should be updated. + $last = REQUEST_TIME - $config->get('translation.update_interval_days') * 3600 * 24; + $query = db_select('locale_file', 'f'); + $query->join('locale_project', 'p', 'p.name = f.project'); + $query->condition('f.last_checked', $last, '<'); + $query->fields('f', array('project', 'langcode')); + if (!$config->get('translation.check_disabled_modules')) { + $query->condition('p.status', 1); + } + $files = $query->execute()->fetchAll(); + foreach ($files as $file) { + $updates[$file->project][] = $file->langcode; + + // Update the last_checked timestamp of the project+language that will + // be checked for updates. + db_update('locale_file') + ->fields(array('last_checked' => REQUEST_TIME)) + ->condition('project', $file->project) + ->condition('langcode', $file->langcode) + ->execute(); + } + + // For each project+language combination a number of tasks are added to + // the queue. + if ($updates) { + module_load_include('fetch.inc', 'locale'); + $options = _locale_translation_default_update_options(); + $queue = Drupal::queue('locale_translation', TRUE); + + foreach ($updates as $project => $languages) { + $batch = locale_translation_batch_update_build(array($project), $languages, $options); + foreach ($batch['operations'] as $item) { + $queue->createItem($item); + } + } + } +} + +/** * Determine if a file is a remote file. * * @param string $uri @@ -354,5 +432,7 @@ function _locale_translation_default_update_options() { 'not_customized' => $config->get('translation.overwrite_not_customized'), 'customized' => $config->get('translation.overwrite_customized'), ), + 'finish_feedback' => TRUE, + 'use_remote' => locale_translation_use_remote_source(), ); }