Commit 936cbb91 authored by webchick's avatar webchick

Issue #1804688 by Sutharsan, YesCT, clemens.tolboom, Gábor Hojtsy, webflo,...

Issue #1804688 by Sutharsan, YesCT, clemens.tolboom, Gábor Hojtsy, webflo, Jose Reyero: Download and import interface translations.
parent 8f50acbc
......@@ -170,7 +170,7 @@ public function open() {
$this->readHeader();
}
else {
throw new Exception('Cannot open stream without URI set.');
throw new \Exception('Cannot open stream without URI set.');
}
}
......@@ -185,7 +185,7 @@ public function close() {
fclose($this->_fd);
}
else {
throw new Exception('Cannot close stream that is not open.');
throw new \Exception('Cannot close stream that is not open.');
}
}
......
......@@ -3,3 +3,7 @@ translation:
check_disabled_modules: false
default_filename: '%project-%version.%language.po'
default_server_pattern: 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po'
overwrite_customized: false
overwrite_not_customized: true
update_interval_days: '0'
......@@ -53,10 +53,10 @@ static function filesToArray($langcode, array $files) {
*
* @param stdClass $file
* File object with an URI property pointing at the file's path.
*
* - "langcode": The language the strings will be added to.
* - "uri": File URI.
* @param array $options
* An array with options that can have the following elements:
* - 'langcode': The language code, required.
* - 'overwrite_options': Overwrite options array as defined in
* Drupal\locale\PoDatabaseWriter. Optional, defaults to an empty array.
* - 'customized': Flag indicating whether the strings imported from $file
......@@ -83,7 +83,7 @@ static function fileToDatabase($file, $options) {
);
// Instantiate and initialize the stream reader for this file.
$reader = new PoStreamReader();
$reader->setLangcode($options['langcode']);
$reader->setLangcode($file->langcode);
$reader->setURI($file->uri);
try {
......@@ -100,7 +100,7 @@ static function fileToDatabase($file, $options) {
// Initialize the database writer.
$writer = new PoDatabaseWriter();
$writer->setLangcode($options['langcode']);
$writer->setLangcode($file->langcode);
$writer_options = array(
'overwrite_options' => $options['overwrite_options'],
'customized' => $options['customized'],
......
......@@ -102,7 +102,7 @@ function getHeader() {
* Always, because you cannot set the PO header of a reader.
*/
function setHeader(PoHeader $header) {
throw new Exception('You cannot set the PO header in a reader.');
throw new \Exception('You cannot set the PO header in a reader.');
}
/**
......
......@@ -160,14 +160,14 @@ function setHeader(PoHeader $header) {
// Check for options.
$options = $this->getOptions();
if (empty($options)) {
throw new Exception("Options should be set before assigning a PoHeader.");
throw new \Exception('Options should be set before assigning a PoHeader.');
}
$overwrite_options = $options['overwrite_options'];
// Check for langcode.
$langcode = $this->_langcode;
if (empty($langcode)) {
throw new Exception("Langcode should be set before assigning a PoHeader.");
throw new \Exception('Langcode should be set before assigning a PoHeader.');
}
if (array_sum($overwrite_options) || empty($locale_plurals[$langcode]['plurals'])) {
......
<?php
/**
* @file
* Definition of Drupal\locale\Tests\LocaleCompareTest.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests for comparing status of existing project translations with available translations.
*/
class LocaleCompareTest extends WebTestBase {
/**
* The path of the translations directory where local translations are stored.
*
* @var string
*/
private $tranlations_directory;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('update', 'locale', 'locale_test');
public static function getInfo() {
return array(
'name' => 'Compare project states',
'description' => 'Tests for comparing status of existing project translations with available translations.',
'group' => 'Locale',
);
}
/**
* Setup the test environment.
*
* We use German as default test language. Due to hardcoded configurations in
* the locale_test module, the language can not be chosen randomly.
*/
function setUp() {
parent::setUp();
module_load_include('compare.inc', 'locale');
$admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'translate interface'));
$this->drupalLogin($admin_user);
$this->drupalPost('admin/config/regional/language/add', array('predefined_langcode' => 'de'), t('Add language'));
}
/**
* Set the value of the default translations directory.
*
* @param string $path
* Path of the translations directory relative to the drupal installation
* directory.
*/
private function setTranslationsDirectory($path) {
$this->tranlations_directory = $path;
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
variable_set('locale_translate_file_directory', $path);
}
/**
* Creates a translation file and tests its timestamp.
*
* @param string $path
* Path of the file relative to the public file path.
* @param string $filename
* Name of the file to create.
* @param integer $timestamp
* Timestamp to set the file to. Defaults to current time.
* @param string $data
* Translation data to put into the file. Po header data will be added.
*/
private function makePoFile($path, $filename, $timestamp = NULL, $data = '') {
$timestamp = $timestamp ? $timestamp : REQUEST_TIME;
$path = 'public://' . $path;
$po_header = <<<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;
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
$file = entity_create('file', array(
'uid' => 1,
'filename' => $filename,
'uri' => $path . '/' . $filename,
'filemime' => 'text/x-gettext-translation',
'timestamp' => $timestamp,
'status' => FILE_STATUS_PERMANENT,
));
file_put_contents($file->uri, $po_header . $data);
touch(drupal_realpath($file->uri), $timestamp);
$file->save();
}
/**
* Test for translation status storage and translation status comparison.
*/
function testLocaleCompare() {
// Create and login user.
$admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages'));
$this->drupalLogin($admin_user);
module_load_include('compare.inc', 'locale');
// Check if hidden modules are not included.
$projects = locale_translation_project_list();
$this->assertFalse(isset($projects['locale_test']), 'Hidden module not found');
// Make the test modules look like a normal custom module. i.e. make the
// modules not hidden. locale_test_system_info_alter() modifies the project
// info of the locale_test and locale_test_disabled modules.
state()->set('locale_translation_test_system_info_alter', TRUE);
// Reset static system list caches to reflect info changes.
drupal_static_reset('locale_translation_project_list');
system_list_reset();
// Check if interface translation data is collected from hook_info.
$projects = locale_translation_project_list();
$this->assertEqual($projects['locale_test']['info']['interface translation server pattern'], 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.');
$this->assertEqual($projects['locale_test']['name'] , 'locale_test', format_string('%key found in project info.', array('%key' => 'interface translation project')));
// Get the locale settings.
$config = config('locale.settings');
// Check if disabled modules are detected.
$config->set('translation.check_disabled_modules', TRUE)->save();
drupal_static_reset('locale_translation_project_list');
$projects = locale_translation_project_list();
$this->assertTrue(isset($projects['locale_test_disabled']), 'Disabled module found');
// Check the fully processed list of project data of both enabled and
// disabled modules.
$config->set('translation.check_disabled_modules', TRUE)->save();
drupal_static_reset('locale_translation_project_list');
$projects = locale_translation_get_projects();
$this->assertEqual($projects['drupal']->name, 'drupal', 'Core project found');
$this->assertEqual($projects['locale_test']->server_pattern, 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.');
$this->assertEqual($projects['locale_test_disabled']->status, '0', 'Disabled module found');
$config->delete('translation.check_disabled_modules');
// Return the locale test modules back to their hidden state.
state()->delete('locale_translation_test_system_info_alter');
}
/**
* Checks if local or remote translation sources are detected.
*
* This test requires a simulated environment for local and remote files.
* Normally remote files are located at a remote server (e.g. ftp.drupal.org).
* For testing we can not rely on this. A directory in the file system of the
* test site is designated for remote files and is addressed using an absolute
* URL. Because Drupal does not allow files with a po extension to be accessed
* (denied in .htaccess) the translation files get a txt extension. Another
* directory is designated for local translation files.
*
* The translation status process by default checks the status of the
* installed projects. For testing purpose a predefined set of modules with
* fixed file names and release versions is used. Using a
* hook_locale_translation_projects_alter implementation in the locale_test
* module this custom project definition is applied.
*
* This test generates a set of local and remote translation files in their
* respective local and remote translation directory. The test checks whether
* the most recent files are selected in the different check scenarios: check
* for local files only, check for remote files only, check for both local and
* remote files.
*/
function testCompareCheckLocal() {
$config = config('locale.settings');
// A flag is set to let the locale_test module replace the project data with
// a set of test projects.
state()->set('locale_translation_test_projects', TRUE);
// Setup timestamps to identify old and new translation sources.
$timestamp_old = REQUEST_TIME - 100;
$timestamp_new = REQUEST_TIME;
// Set up the environment.
$public_path = variable_get('file_public_path', conf_path() . '/files');
$this->setTranslationsDirectory($public_path . '/local');
$config->set('translation.default_filename', '%project-%version.%language.txt')->save();
// Add a number of files to the local file system to serve as remote
// translation server and match the project definitions set in
// locale_test_locale_translation_projects_alter().
$this->makePoFile('remote/8.x/contrib_module_one', 'contrib_module_one-8.x-1.1.de.txt', $timestamp_new);
$this->makePoFile('remote/8.x/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de.txt', $timestamp_old);
$this->makePoFile('remote/8.x/contrib_module_three', 'contrib_module_three-8.x-1.0.de.txt', $timestamp_old);
// Add a number of files to the local file system to serve as local
// translation files and match the project definitions set in
// locale_test_locale_translation_projects_alter().
$this->makePoFile('local', 'contrib_module_one-8.x-1.1.de.txt', $timestamp_old);
$this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de.txt', $timestamp_new);
$this->makePoFile('local', 'custom_module_one.de.po', $timestamp_new);
// Get status of translation sources at local file system.
$config->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)->save();
$this->drupalGet('admin/reports/translations/check');
$result = state()->get('locale_translation_status');
$this->assertEqual($result['contrib_module_one']['de']->type, 'local', 'Translation of contrib_module_one found');
$this->assertEqual($result['contrib_module_one']['de']->timestamp, $timestamp_old, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_two']['de']->type, 'local', 'Translation of contrib_module_two found');
$this->assertEqual($result['contrib_module_two']['de']->timestamp, $timestamp_new, 'Translation timestamp found');
$this->assertEqual($result['locale_test']['de']->type, 'local', 'Translation of locale_test found');
$this->assertEqual($result['custom_module_one']['de']->type, 'local', 'Translation of custom_module_one found');
// Get status of translation sources at both local and remote the locations.
$config->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL)->save();
$this->drupalGet('admin/reports/translations/check');
$result = state()->get('locale_translation_status');
$this->assertEqual($result['contrib_module_one']['de']->type, 'remote', 'Translation of contrib_module_one found');
$this->assertEqual($result['contrib_module_one']['de']->timestamp, $timestamp_new, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_two']['de']->type, 'local', 'Translation of contrib_module_two found');
$this->assertEqual($result['contrib_module_two']['de']->timestamp, $timestamp_new, 'Translation timestamp found');
$this->assertEqual($result['contrib_module_three']['de']->type, 'remote', 'Translation of contrib_module_three found');
$this->assertEqual($result['contrib_module_three']['de']->timestamp, $timestamp_old, 'Translation timestamp found');
$this->assertEqual($result['locale_test']['de']->type, 'local', 'Translation of locale_test found');
$this->assertEqual($result['custom_module_one']['de']->type, 'local', 'Translation of custom_module_one found');
}
}
<?php
/**
* @file
* Definition of Drupal\locale\Tests\LocaleFileImportStatus.
*/
namespace Drupal\locale\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Functional tests for the import of translation files.
*/
class LocaleFileImportStatus extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale');
public static function getInfo() {
return array(
'name' => 'Translation file import status',
'description' => 'Tests the status of imported translation files.',
'group' => 'Locale',
);
}
function setUp() {
parent::setUp();
// Create and login user.
$admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages'));
$this->drupalLogin($admin_user);
// Copy test po files to the translations directory.
file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.de.po', 'translations://', FILE_EXISTS_REPLACE);
file_unmanaged_copy(drupal_get_path('module', 'locale') . '/tests/test.xx.po', 'translations://', FILE_EXISTS_REPLACE);
}
/**
* Add a language.
*
* @param $langcode
* The language of the langcode to add.
*/
function addLanguage($langcode) {
$edit = array('predefined_langcode' => $langcode);
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
drupal_static_reset('language_list');
$this->assertTrue(language_load($langcode), t('Language %langcode added.', array('%langcode' => $langcode)));
}
/**
* Get translations for an array of strings.
*
* @param $strings
* An array of strings to translate.
* @param $langcode
* The language code of the language to translate to.
*/
function checkTranslations($strings, $langcode) {
foreach ($strings as $source => $translation) {
$db_translation = db_query('SELECT translation FROM {locales_target} lt INNER JOIN {locales_source} ls ON ls.lid = lt.lid WHERE ls.source = :source AND lt.language = :langcode', array(':source' => $source, ':langcode' => $langcode))->fetchField();
$this->assertEqual((string) $translation, (string) $db_translation);
}
}
/**
* Import a single interface translation file.
*
* @param $langcode
* Langcode of the po file and language to import.
* @param int $timestamp_difference
* (optional) Timestamp offset, used to mock older or newer files.
*
* @return stdClass
* A file object of type stdClass.
*/
function mockImportedPoFile($langcode, $timestamp_difference = 0) {
$testfile_uri = 'translations://test.' . $langcode . '.po';
$file = locale_translate_file_create($testfile_uri);
$file->original_timestamp = $file->timestamp;
$file->timestamp = $file->timestamp + $timestamp_difference;
$file->langcode = $langcode;
// Fill the {locale_file} with a custom timestamp.
if ($timestamp_difference != 0) {
locale_translate_update_file_history($file);
}
$count = db_query('SELECT COUNT(*) FROM {locale_file} WHERE langcode = :langcode', array(':langcode' => $langcode))->fetchField();
$this->assertEqual(1, $count, format_plural($count, '@count file registered in {locale_file}.', '@count files registered in {locale_file}.'));
$result = db_query('SELECT langcode, uri FROM {locale_file}')->fetchAssoc();
$this->assertEqual($result['uri'], $testfile_uri, t('%uri is in {locale_file}.', array('%uri' => $result['uri'])));
$this->assertEqual($result['langcode'], $langcode, t('Langcode is %langcode.', array('%langcode' => $langcode)));
return $file;
}
/**
* Test the basic bulk import functionality.
*/
function testBulkImport() {
$langcode = 'de';
// Translations should not exist.
$strings = array(
'Monday' => '',
'Tuesday' => '',
);
$this->checkTranslations($strings, $langcode);
// Add language.
$this->addLanguage($langcode);
// The file was imported, translations should exist.
$strings = array(
'Monday' => 'Montag',
'Tuesday' => 'Dienstag',
);
$this->checkTranslations($strings, $langcode);
}
/**
* Update a pre-existing file.
*/
function testBulkImportUpdateExisting() {
$langcode = 'de';
// Translations should not exist.
$strings = array(
'Monday' => '',
'Tuesday' => '',
);
$this->checkTranslations($strings, $langcode);
// Fill the {locale_file} table with an older file.
$file = $this->mockImportedPoFile($langcode, -1);
// Add language.
$this->addLanguage($langcode);
// The file was imported, translations should exist.
$strings = array(
'Monday' => 'Montag',
'Tuesday' => 'Dienstag',
);
$this->checkTranslations($strings, $langcode);
$timestamp = db_query('SELECT timestamp FROM {locale_file} WHERE uri = :uri', array(':uri' => $file->uri))->fetchField();
// Ensure that $timestamp is now greater than the manipulated timestamp.
$this->assertTrue($timestamp > $file->timestamp, 'Timestamp on locale_file updated.');
}
/**
* Don't update a pre-existing file.
*/
function testBulkImportNotUpdateExisting() {
$langcode = 'de';
// Translations should not exist.
$strings = array(
'Monday' => '',
'Tuesday' => '',
);
$this->checkTranslations($strings, $langcode);
// Fill the {locale_file} table with a newer file.
$file = $this->mockImportedPoFile($langcode, 1);
// Add language.
$this->addLanguage($langcode);
// The file was not imported, the translation should not exist.
$strings = array(
'Monday' => '',
'Tuesday' => '',
);
$this->checkTranslations($strings, $langcode);
$timestamp = db_query('SELECT timestamp FROM {locale_file} WHERE uri = :uri', array(':uri' => $file->uri))->fetchField();
$this->assertEqual($timestamp, $file->timestamp);
}
/**
* Delete translation files after deleting a language.
*/
function testDeleteLanguage() {
$langcode = 'de';
$this->addLanguage($langcode);
$file_uri = 'translations://po_' . $this->randomName() . '.' . $langcode . '.po';
file_put_contents(drupal_realpath($file_uri), $this->randomName());
$this->assertTrue(is_file($file_uri), 'Translation file is created.');
language_delete($langcode);
$this->assertTrue($file_uri);
$this->assertFalse(is_file($file_uri), 'Translation file deleted after deleting language');
}
}
......@@ -58,7 +58,7 @@ function testStandalonePoFile() {
$this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), t('The language has been automatically created.'));
// The import should have created 8 strings.
$this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
// This import should have saved plural forms to have 2 variants.
$locale_plurals = variable_get('locale_translation_plurals', array());
......@@ -74,8 +74,9 @@ function testStandalonePoFile() {
));
// The import should have created 1 string and rejected 2.
$this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
$skip_message = format_plural(2, 'A translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
$skip_message = format_plural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
$this->assertRaw($skip_message, t('Unsafe strings were skipped.'));
// Try importing a zero byte sized .po file.
......@@ -84,7 +85,7 @@ function testStandalonePoFile() {
));
// The import should have created 0 string and rejected 0.
$this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 0)), 'The empty translation file was successfully imported.');
$this->assertRaw(t('One translation file could not be imported. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog'))), 'The empty translation file was successfully imported.');
// Try importing a .po file which doesn't exist.
$name = $this->randomName(16);
......@@ -103,7 +104,7 @@ function testStandalonePoFile() {
));
// The import should have created 1 string.
$this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
// Ensure string wasn't overwritten.
$search = array(
'string' => 'Montag',
......@@ -125,7 +126,7 @@ function testStandalonePoFile() {
));
// The import should have updated 2 strings.
$this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.'));
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.'));
// Ensure string was overwritten.
$search = array(
'string' => 'Montag',
......@@ -145,7 +146,7 @@ function testStandalonePoFile() {
));
// The import should have created 6 strings.
$this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 6, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.'));
$this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', array('%number' => 6, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.'));