diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php new file mode 100644 index 0000000000000000000000000000000000000000..25d5e71a5540349ce697e4f5cc340f8360e30d07 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleCompareTest.php @@ -0,0 +1,77 @@ + 'Compare project states', + 'description' => 'Tests for comparing status of existing project translations with available translations.', + 'group' => 'Locale', + ); + } + + /** + * 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. + variable_set('locale_translation_test_system_info_alter', TRUE); + + // Check if interface translation data is collected from hook_info. + drupal_static_reset('locale_translation_project_list'); + $projects = locale_translation_project_list(); + $this->assertEqual($projects['locale_test']['info']['interface translation server pattern'], 'core/modules/locale/test/modules/locale_test/%project-%version.%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'))); + + // Check if disabled modules are detected. + variable_set('locale_translation_check_disabled', TRUE); + 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. + variable_set('locale_translation_check_disabled', TRUE); + 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/modules/locale_test/%project-%version.%language.po', 'Interface translation parameter found in project info.'); + $this->assertEqual($projects['locale_test_disabled']->status, '0', 'Disabled module found'); + variable_del('locale_translation_check_disabled'); + + // Return the locale test modules back to their hidden state. + variable_del('locale_translation_test_system_info_alter'); + } + +} diff --git a/core/modules/locale/locale.api.php b/core/modules/locale/locale.api.php new file mode 100644 index 0000000000000000000000000000000000000000..9aabb0e4d86bb5f2251112b252ecc7fa230204fd --- /dev/null +++ b/core/modules/locale/locale.api.php @@ -0,0 +1,111 @@ + array( + 'interface translation server pattern' => 'http://example.com/files/translations/%core/%project/%project-%version.%language.po', + ), + ); +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc new file mode 100644 index 0000000000000000000000000000000000000000..55597d1a48443a6a32cc2c9b52606fc84413ddeb --- /dev/null +++ b/core/modules/locale/locale.compare.inc @@ -0,0 +1,265 @@ +rowCount() == 0 && module_exists('update')) { + // At least the core project should be in the database, so we build the + // data if none are found. + locale_translation_build_projects(); + $result = db_query('SELECT * FROM {locale_project}'); + } + + foreach ($result as $project) { + $projects[$project->name] = $project; + } + } + return $projects; +} + +/** + * Clear the project data table. + */ +function locale_translation_flush_projects() { + db_truncate('locale_project')->execute(); +} + +/** + * Builds list of projects and stores the result in the database. + * + * The project data is based on the project list supplied by the Update module. + * Only the properties required by Locale module is included and additional + * (custom) modules and translation server data is added. + * + * In case the Update module is disabled this function will return an empty + * array. + * + * @return array + * Array of project data: + * - "name": Project system name. + * - "project_type": Project type, e.g. 'module', 'theme'. + * - "core": Core release version, e.g. 8.x + * - "version": Project release version, e.g. 8.x-1.0 + * - "server_pattern": Translation server po file pattern. + * - "status": Project status, 1 = enabled. + */ +function locale_translation_build_projects() { + // This function depends on Update module. We degrade gracefully. + if (!module_exists('update')) { + return array(); + } + + // Get the project list based on .info files. + $projects = locale_translation_project_list(); + + // Mark all previous projects as disabled and store new project data. + db_update('locale_project') + ->fields(array( + 'status' => 0, + )) + ->execute(); + + $default_server = locale_translation_default_translation_server(); + + $project_updates = update_get_available(TRUE); + foreach ($projects as $name => $data) { + if (isset($project_updates[$name]['releases']) && $project_updates[$name]['project_status'] != 'not-fetched') { + // Find out if a dev version is installed. + if (preg_match("/^[0-9]+\.x-([0-9]+)\..*-dev$/", $data['info']['version'], $matches)) { + // Find a suitable release to use as alternative translation. + foreach ($project_updates[$name]['releases'] as $project_release) { + // The first release with the same major release number which is not a + // dev release is the one. Releases are sorted the most recent first. + if ($project_release['version_major'] == $matches[1] && + (!isset($project_release['version_extra']) || $project_release['version_extra'] != 'dev')) { + $release = $project_release; + break; + } + } + } + elseif ($name == "drupal" || preg_match("/HEAD/", $data['info']['version'], $matches)) { + // Pick latest available release. + $release = array_shift($project_updates[$name]['releases']); + } + + if (!empty($release['version'])) { + $data['info']['version'] = $release['version']; + } + + unset($release); + } + + $data += array( + 'version' => isset($data['info']['version']) ? $data['info']['version'] : '', + 'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY, + // A project can provide the path and filename pattern to download the + // gettext file. Use the default if not. + 'server_pattern' => isset($data['info']['interface translation server pattern']) ? $data['info']['interface translation server pattern'] : $default_server['pattern'], + 'status' => $data['project_status'] ? 1 : 0, + ); + $project = (object) $data; + $projects[$name] = $project; + + // Create or update the project record. + db_merge('locale_project') + ->key(array('name' => $project->name)) + ->fields(array( + 'name' => $project->name, + 'project_type' => $project->project_type, + 'core' => $project->core, + 'version' => $project->version, + 'server_pattern' => $project->server_pattern, + 'status' => $project->status, + )) + ->execute(); + } + return $projects; +} + +/** + * Fetch an array of projects for translation update. + * + * @return array + * Array of project data including .info file data. + */ +function locale_translation_project_list() { + // This function depends on Update module. We degrade gracefully. + if (!module_exists('update')) { + return array(); + } + + $projects = &drupal_static(__FUNCTION__, array()); + if (empty($projects)) { + module_load_include('compare.inc', 'update'); + $projects = array(); + + $additional_whitelist = array( + 'interface translation project', + 'interface translation server pattern', + ); + $module_data = _locale_translation_prepare_project_list(system_rebuild_module_data(), 'module'); + $theme_data = _locale_translation_prepare_project_list(system_rebuild_theme_data(), 'theme'); + update_process_info_list($projects, $module_data, 'module', TRUE, $additional_whitelist); + update_process_info_list($projects, $theme_data, 'theme', TRUE, $additional_whitelist); + if (variable_get('locale_translation_check_disabled', 0)) { + update_process_info_list($projects, $module_data, 'module', FALSE, $additional_whitelist); + update_process_info_list($projects, $theme_data, 'theme', FALSE, $additional_whitelist); + } + + // Allow other modules to alter projects before fetching and comparing. + drupal_alter('locale_translation_projects', $projects); + } + return $projects; +} + +/** + * Prepare module and theme data. + * + * Modify .info file data before it is processed by update_process_info_list(). + * In order for update_process_info_list() to recognize a project, it requires + * the 'project' parameter in the .info file data. + * Custom modules or themes can bring their own gettext translation file. To + * enable import of this file the module or theme defines "interface translation + * project = myproject" in its .info file. This function will add a project + * "myproject" to the info data. + * + * @param array $data + * Array of .info file data. + * @param string $type + * The project type. i.e. module, theme. + * + * @return array + * Array of .info file data. + */ +function _locale_translation_prepare_project_list($data, $type) { + foreach ($data as $name => $file) { + // Include interface translation projects. + // Custom modules can bring their own gettext translation file. + // To enable import of this file the module must define + // 'interface translation project = myproject' in its .info file. + // To allow update_process_info_list() to identify this as a project + // the 'project' property is filled with the 'interface translation project' + // value. + if (isset($file->info['interface translation project'])) { + $data[$name]->info['project'] = $file->info['interface translation project']; + } + } + return $data; +} + +/** + * Retrieve data for default server. + * + * @return array + * Array of server parameters: + * - "server_pattern": URL containing po file pattern. + */ +function locale_translation_default_translation_server() { + return array( + 'pattern' => variable_get('locale_translation_default_server_pattern', LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN), + ); +} + +/** + * 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. + * - "%filename": Project file name. + * + * @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->language) ? $project->language : '%language', + '%filename' => isset($project->filename) ? $project->filename : '%filename', + ); + return strtr($template, $variables); +} diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index 334b401ec038f6490404aca0a1799015c7844458..35c8185a73e5f584e9980e02b8405aaf3043ae3f 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -32,6 +32,9 @@ function locale_uninstall() { variable_del('locale_cache_length'); variable_del('locale_translation_plurals'); variable_del('locale_translation_javascript'); + variable_del('locale_translation_test_system_info_alter'); + variable_del('locale_translation_check_disabled'); + variable_del('locale_translation_default_server_pattern'); // Remove all node type language variables. Node module might have been // enabled, but may be disabled, so use a wildcard delete. @@ -159,6 +162,55 @@ function locale_schema() { 'primary key' => array('uri', 'langcode'), ); + $schema['locale_project'] = array( + 'description' => 'Translation status information for projects and server data.', + 'fields' => array( + 'name' => array( + 'description' => 'A unique short name to identify the project.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'project_type' => array( + 'description' => 'Project type, may be core, module, theme', + 'type' => 'varchar', + 'length' => 15, + 'not null' => TRUE, + ), + 'core' => array( + 'description' => 'Core compatibility string for this project.', + 'type' => 'varchar', + 'length' => 4, + 'not null' => TRUE, + 'default' => '', + ), + 'version' => array( + 'description' => 'The version release of the project.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'server_pattern' => array( + 'description' => 'Pattern of path and name of the gettext file at the translation server.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'status' => array( + 'description' => 'The update status of the project.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'primary key' => array('name'), + ); + + $schema['cache_locale'] = drupal_get_schema_unprocessed('system', 'cache'); + $schema['cache_locale']['description'] = 'Cache table for the locale module to store various data.'; + return $schema; } @@ -622,6 +674,113 @@ function locale_update_8010() { db_create_table('locale_file', $table); } +/** + * Add a cache table and locale_project table for the locale module. + */ +function locale_update_8011() { + // Add a 'locale' cache table. + db_create_table('cache_locale', array( + 'description' => 'Cache table for the locale module to store various data.', + 'fields' => array( + 'cid' => array( + 'description' => 'Primary Key: Unique cache ID.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'data' => array( + 'description' => 'A collection of data to cache.', + 'type' => 'blob', + 'not null' => FALSE, + 'size' => 'big', + ), + 'expire' => array( + 'description' => 'A Unix timestamp indicating when the cache entry should expire, or 0 for never.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'created' => array( + 'description' => 'A Unix timestamp indicating when the cache entry was created.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'serialized' => array( + 'description' => 'A flag to indicate whether content is serialized (1) or not (0).', + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + 'default' => 0, + ), + 'tags' => array( + 'description' => 'Space-separated list of cache tags for this entry.', + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'checksum' => array( + 'description' => 'The tag invalidation sum when this entry was saved.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'expire' => array('expire'), + ), + 'primary key' => array('cid'), + )); + + // Add locale_project table. + db_create_table('locale_project', array( + 'description' => 'Translation status information for projects and server data.', + 'fields' => array( + 'name' => array( + 'description' => 'A unique short name to identify the project.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'project_type' => array( + 'description' => 'Project type, may be core, module, theme', + 'type' => 'varchar', + 'length' => 15, + 'not null' => TRUE, + ), + 'core' => array( + 'description' => 'Core compatibility string for this project.', + 'type' => 'varchar', + 'length' => 4, + 'not null' => TRUE, + 'default' => '', + ), + 'version' => array( + 'description' => 'The release version of the project.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'server_pattern' => array( + 'description' => 'Pattern of path and name of the gettext file at the translation server.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'status' => array( + 'description' => 'The update status of the project.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 1, + ), + ), + 'primary key' => array('name'), + )); +} + /** * @} End of "addtogroup updates-7.x-to-8.x". * The next series of updates should start at 9000. diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.info b/core/modules/locale/tests/modules/locale_test/locale_test.info new file mode 100644 index 0000000000000000000000000000000000000000..1ec7508b72514b69bec30a317b218dae1311bc5b --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test/locale_test.info @@ -0,0 +1,10 @@ +name = Locale test +description = Support module for locale module testing. +package = Testing +version = 1.2 +core = 8.x +hidden = TRUE + +; Definitions for interface translations. +interface translation project = locale_test +interface translation server pattern = core/modules/locale/test/modules/locale_test/%project-%version.%language.po diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.module b/core/modules/locale/tests/modules/locale_test/locale_test.module new file mode 100644 index 0000000000000000000000000000000000000000..6044a076955c947c499f88c164bc6682169bb624 --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test/locale_test.module @@ -0,0 +1,27 @@ +name == 'locale_test' || $file->name == 'locale_test_disabled') { + // Make the module appear as not-disabled. + $info['hidden'] = FALSE; + } +} diff --git a/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.info b/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.info new file mode 100644 index 0000000000000000000000000000000000000000..7eddf25be3855a3e192617cb20e7f9b234e7ce31 --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.info @@ -0,0 +1,10 @@ +name = Disabled locale test +description = Disabled support module for locale module testing. +package = Testing +version = VERSION +core = 8.x +hidden = TRUE +project = locale_test_disabled + +; Definitions for interface translation. +interface translation project = locale_test_disabled diff --git a/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.module b/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.module new file mode 100644 index 0000000000000000000000000000000000000000..a80d9dac768b91130dde443eea9146f17a872261 --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.module @@ -0,0 +1,6 @@ +