Commit c1f21810 authored by alexpott's avatar alexpott

Issue #1998056 by Sutharsan, clemens.tolboom, penyaskito: Automatically update...

Issue #1998056 by Sutharsan, clemens.tolboom, penyaskito: Automatically update interface translations using cron.
parent b07e8f07
......@@ -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. <a href="@url">Check updates now</a>.', array('@url' => url('admin/reports/translations/check'))),
);
......
<?php
/**
* @file
* Contains Drupal\locale\Tests\LocaleUpdateTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests for update translations.
*/
class LocaleUpdateBase extends WebTestBase {
/**
* The path of the translations directory where local translations are stored.
*
* @var string
*/
protected $tranlations_directory;
/**
* Timestamp for an old translation.
*
* @var integer
*/
protected $timestamp_old;
/**
* Timestamp for a medium aged translation.
*
* @var integer
*/
protected $timestamp_medium;
/**
* Timestamp for a new translation.
*
* @var integer
*/
protected $timestamp_new;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('update', 'locale', 'locale_test');
function setUp() {
parent::setUp();
// 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.
*/
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 = <<<EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 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 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)));
}
}
<?php
/**
* @file
* Contains Drupal\locale\Tests\LocaleUpdateCronTest.
*/
namespace Drupal\locale\Tests;
/**
* Tests for translation update using cron.
*/
class LocaleUpdateCronTest extends LocaleUpdateBase {
protected $batch_output = array();
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('update', 'locale', 'locale_test');
public static function getInfo() {
return array(
'name' => '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);
$this->assertEqual($queue->numberOfItems(), 3, 'Queue holds tasks for one project.');
$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(), 3, 'Queue holds tasks for one project.');
// 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');
}
}
......@@ -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. <a href="@add_language">Add a language</a> 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.
......
This diff is collapsed.
......@@ -206,31 +206,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.
*
......@@ -242,6 +217,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
......@@ -251,6 +227,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);
}
}
......@@ -276,7 +253,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
......@@ -294,8 +271,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,
......@@ -312,30 +290,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;
}
......@@ -343,8 +317,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