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 0000000..2fe13ba --- /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', + ); + } + + /** + * Tets for translation status storage and translation status compare. + */ + 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']), t('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', t('Interface translation parameter found in project info.')); + $this->assertEqual($projects['locale_test']['name'] , 'locale_test', t('%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']), t('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', t('Core project found')); + $this->assertEqual($projects['locale_test']->server_pattern, 'core/modules/locale/test/modules/locale_test/%project-%version.%language.po', t('Interface translation parameter found in project info.')); + $this->assertEqual($projects['locale_test_disabled']->status, '0', t('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 0000000..e1ad3a1 --- /dev/null +++ b/core/modules/locale/locale.api.php @@ -0,0 +1,104 @@ + array( + 'interface translation server' => 'example.com', + 'interface translation server url' => 'http://example.com/files/translations/l10n_server.xml', + 'interface translation server pattern' => 'http://example.com/files/translations/%core/%project/%project-%release.%language.po', + ), + ); +} + +/** + * Allows to define or alter project definitions for interface translation. + * + * Themes can implement this hook too in their template.php file. + * + * @return + * Array of project defintions. + */ +function hook_locale_translation_additional_project_info() { + // If your custom module contains new strings the Locale interface translation + // can be configured to recognize and import the translations. + // The tanslations can be located in the local file system or remotely in a + // translation server (similar to localize.drupal.org). + // Required: type. "project" is required if the "example_project" is a custom + // module and not a contributed module. + + // Po file located at a remote translation server. + $projects['example_module'] = array( + 'type' => 'module', + 'info' => array( + 'project' => 'example_module', + 'interface translation server' => 'example.com', + 'interface translation server url' => 'http://example.com/files/translations/l10n_server.xml', + 'interface translation server pattern' => 'http://example.com/files/translations/%core/%project/%project-%version.%language.po', + ), + ); + + // Po file located in local file system. + $projects['example_module'] = array( + 'type' => 'module', + 'info' => array( + 'project' => 'example_module', + 'interface translation server pattern' => 'sites/example.com/modules/custom/example_module/%project-%version.%language.po', + ), + ); + + // When multiple custom modules share the same po file, their project + // definitions should match. Both "project", "version" and "interface + // translation server" definitions are used as part of the po file format and + // should match. + // In this example the above example module and the other_example_module share + // the same po file. "project", "version" and "interface translation server + // pattern" are overridden to match the above po file name as provided by the + // example_module. + $projects['other_example_module'] = array( + 'type' => 'module', + 'info' => array( + 'project' => 'example_module', + 'version' => '1.1', + 'interface translation server pattern' => 'sites/example.com/modules/custom/example_module/%project-%version.%language.po', + ), + ); + + // Themes can implement this hook too. + $projects['zen'] = array( + 'type' => 'theme', + 'info' => array( + 'interface translation server' => 'example.com', + 'interface translation server url' => 'http://example.com/files/translations/l10n_server.xml', + 'interface translation server pattern' => 'http://example.com/files/translations/%core/%project/%project-%version.%language.po', + ), + ); + + return $projects; +} + +// @todo Remove this list. +// Whish list +// ========== +// Let the translation server provide the name and pattern details, instead +// of defining it in the .info file or in code. +// Will this work if an installation profile or a custom module provides +// it's own info hook and needs to download translations from a remote server? diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc new file mode 100644 index 0000000..10bfaa7 --- /dev/null +++ b/core/modules/locale/locale.compare.inc @@ -0,0 +1,401 @@ +rowCount() == 0) { + // 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(); +} + +/** + * Build list of projects and stores the result in the database. + */ +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(); + + $transaction = db_transaction(); + + // 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, + // The project can have its own interface translation server, we use + // default if not. + 'server' => isset($data['info']['interface translation server']) ? $data['info']['interface translation server'] : '', + // A project can provide the server url to fetch metadata, or the path and + // filename pattern to download the gettext file. + 'server_url' => isset($data['info']['interface translation server url']) ? $data['info']['interface translation server url'] : '', + 'server_pattern' => isset($data['info']['interface translation server pattern']) ? $data['info']['interface translation server pattern'] : '', + 'status' => $data['project_status'] ? 1 : 0, + ); + $project = (object) $data; + // Unless the project provides a full filename pattern, we try to build one. + if (!isset($project->server_pattern)) { + $server = NULL; + if ($project->server || $project->server_url) { + $server = locale_translation_translation_server($project->server, $project->server_url); + } + else { + // Use the default server. + $server = locale_translation_translation_server($default_server['server'], $default_server['server_url']); + } + if ($server) { + // Build the update path for this project, with project name and release + // replaced. + $project->server_pattern = locale_translation_build_server_pattern($project, $server['update_url']); + } + } + $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' => $project->server, + 'server_url' => $project->server_url, + '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 server', + 'interface translation server url', + 'interface translation server pattern', + 'interface translation project', + ); + $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) { + $default = array( + 'name' => '', + 'description' => '', + 'type' => '', + 'version' => '', + 'core' => '', + 'status' => '0', + ); + + $projects = locale_translation_get_additional_projects(); + // Merge and add additional project data into the system data. + foreach ($projects as $name => $project) { + if ($type == $project['type']) { + if (isset($data[$name])) { + $data[$name] = (object)array_merge($project, (array)$data[$name]); + $data[$name]->info = array_merge($project['info'], $data[$name]->info); + } + else { + $default['status'] = module_exists($name) ? '1' : '0'; + $data[$name] = (object)array_merge($default, $project); + $data[$name]->info = $project['info']; + } + } + } + return $data; +} + +/** + * Fetch additional project definitions and definition overrides. + * + * @return array + * Array of custom projects. + * + * @see hook_locale_translation_additional_project_info(). + */ +function locale_translation_get_additional_projects() { + $projects = &drupal_static(__FUNCTION__, array()); + + if (empty($projects)) { + $hook = 'locale_translation_additional_project_info'; + + // Call hook_locale_translation_additional_project_info for modules. + $projects = module_invoke_all($hook); + + // Call hook_locale_translation_additional_project_info for themes. + foreach (list_themes() as $theme => $info) { + $path = drupal_get_path('theme', $theme) . '/template.php'; + if (is_file($path)) { + include_once $path; + $function = $theme . '_' . $hook; + if (function_exists($function)) { + $projects += $function(); + } + } + } + } + + return $projects; +} + +/** + * Retrieve data for default server. + * + * @return array + * Array of server parameters: + * - "server": Localization server name + * - "server_url": Localization server URL where language list can be + * retrieved. + * - "server_pattern": URL containing po file pattern. + */ +// @todo: Rework the individual vars to a hook_locale_translation_server_info() with sets of server definitions? +function locale_translation_default_translation_server() { + return array( + 'server' => variable_get('locale_translation_default_server', LOCALE_TRANSLATION_DEFAULT_SERVER), + 'server_url' => variable_get('locale_translation_default_server_url', LOCALE_TRANSLATION_DEFAULT_SERVER_URL), + 'server_pattern' => variable_get('locale_translation_default_server_pattern', LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN), + ); +} + +/** + * Get server information, that can come from different sources. + * + * Possible server sources: + * - From server list provided by modules. They can provide full server + * information or just the URL. + * - From server_url in a project, we'll fetch the latest data from the server. + * + * @param string $name + * Server name e.g. localize.drupal.org + * @param string $url + * Server url + * @param bool $refresh + * TRUE = refresh the server data. + * + * @return array + * Array of server data. + */ +function locale_translation_translation_servers($name = NULL, $url = NULL, $refresh = FALSE) { + $info = &drupal_static(__FUNCTION__, array()); + $server_list = &drupal_static(__FUNCTION__ . ':server_list', array()); + + // Retrieve server list from modules. + if (!isset($server_list) || $refresh) { + $server_list = module_invoke_all('locale_translation_translation_servers'); + } + // We need at least the server url to fetch all the information. + if (!$url && $name && isset($server_list[$name])) { + $url = $server_list[$name]['server_url']; + } + // If we still don't have a URL, cannot find this server, return false. + if (!$url) { + return FALSE; + } + // Cache server information based on the URL, refresh if asked. + $cid = 'interface_translation_server:' . $url; + if ($refresh) { + unset($info); + cache('locale')->delete($cid); + } + if (!isset($info[$url])) { + if ($cache = cache('locale')->get($cid)) { + $info[$url] = $cache->data; + } + else { + module_load_include('fetch.inc', 'locale'); + if ($name && !empty($server_list[$name])) { + // The name is in our list, it can be full data or just an url. + $server = $server_list[$name]; + } + else { + // This may be a new server provided by a module / package. + $server = array('name' => $name, 'server_url' => $url); + // If searching by name, store the name => url mapping. + if ($name) { + $server_list[$name] = $server; + } + } + // Now fetch server meta information form the server itself. + module_load_include('fetch.inc', 'locale'); + if ($server = locale_translation_get_server($server)) { + cache('locale')->set($cid, $server); + $info[$url] = $server; + } + else { + // If no server information, this will be FALSE. We won't search a + // server twice. + $info[$url] = FALSE; + } + } + } + return $info[$url]; +} + +/** + * 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 place holders. Available placeholders: + * - "%project": Project name. + * - "%version": Poject version. + * - "%core": Project core version. + * - "%language": Language code. + * - "%filename": Project file name. + * + * @return string + * String with replaced place holders. + */ +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 334b401..fc9d4a0 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -32,6 +32,7 @@ function locale_uninstall() { variable_del('locale_cache_length'); variable_del('locale_translation_plurals'); variable_del('locale_translation_javascript'); + // @todo Delete locale_translation_ variables. // Remove all node type language variables. Node module might have been // enabled, but may be disabled, so use a wildcard delete. @@ -159,6 +160,73 @@ function locale_schema() { 'primary key' => array('uri', 'langcode'), ); + $schema['locale_project'] = array( + 'description' => 'Update information for project translations.', + 'fields' => array( + 'name' => array( + 'description' => 'A unique short name to identify the project.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'project_type' => array( + 'description' => 'Project type, may be core, module, theme', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'core' => array( + 'description' => 'Core compatibility string for this project.', + 'type' => 'varchar', + 'length' => '128', + 'not null' => TRUE, + 'default' => '', + ), + 'version' => array( + 'description' => 'Human readable name for project used on the interface.', + 'type' => 'varchar', + 'length' => '128', + 'not null' => TRUE, + 'default' => '', + ), + 'server' => array( + 'description' => 'Translation server for this project. If empty, the default is used.', + 'type' => 'varchar', + 'length' => '255', + 'not null' => TRUE, + 'default' => '', + ), + 'server_url' => array( + 'description' => 'URL of the xml file at the translation server containing all available languages.', + 'type' => 'varchar', + 'length' => '255', + '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' => 'Status flag. TBD', + '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.'; + + // @todo Enable if required. + //$schema['cache_locale_update'] = drupal_get_schema_unprocessed('system', 'cache'); + //$schema['cache_locale_update']['description'] = 'Cache table for the Localization Update module to store information about available releases, fetched from central server.'; + return $schema; } @@ -586,7 +654,7 @@ function locale_update_8009() { /** * Add {locale_file} table. */ -function locale_update_8010() { +function locale_update_8011() { $table = array( 'description' => 'File import status information for interface translation files.', 'fields' => array( @@ -623,6 +691,77 @@ function locale_update_8010() { } /** + * Add a cache table and locale_project table for the locale module. + */ +function locale_update_8010() { + // Add a 'locale' cache table. + $table = drupal_get_schema_unprocessed('system', 'cache'); + $table['description'] = 'Cache table for the locale module to store various data.'; + db_create_table('cache_locale', $table); + + // Add locale_project table. + db_create_table('locale_project', array( + 'description' => 'Update information for project translations.', + 'fields' => array( + 'name' => array( + 'description' => 'A unique short name to identify the project.', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'project_type' => array( + 'description' => 'Project type, may be core, module, theme', + 'type' => 'varchar', + 'length' => '50', + 'not null' => TRUE, + ), + 'core' => array( + 'description' => 'Core compatibility string for this project.', + 'type' => 'varchar', + 'length' => '128', + 'not null' => TRUE, + 'default' => '', + ), + 'version' => array( + 'description' => 'Human readable name for project used on the interface.', + 'type' => 'varchar', + 'length' => '128', + 'not null' => TRUE, + 'default' => '', + ), + 'server' => array( + 'description' => 'Translation server for this project. If empty, the default is used.', + 'type' => 'varchar', + 'length' => '255', + 'not null' => TRUE, + 'default' => '', + ), + 'server_url' => array( + 'description' => 'URL of the xml file at the translation server containing all available languages.', + 'type' => 'varchar', + 'length' => '255', + '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' => 'Status flag. TBD', + '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-1.2.nl.po b/core/modules/locale/tests/modules/locale_test/locale_test-1.2.nl.po new file mode 100644 index 0000000..e69de29 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 0000000..7cf4200 --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test/locale_test.info @@ -0,0 +1,6 @@ +name = Locale test +description = Support module for locale module testing. +package = Testing +version = 1.2 +core = 8.x +hidden = TRUE 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 0000000..bb47741 --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test/locale_test.module @@ -0,0 +1,44 @@ +name == 'locale_test' || $file->name == 'locale_test_disabled') { + // Make the module appear as not-disabled. + $info['hidden'] = FALSE; + } +} + +/** + * Implements hook_locale_translation_additional_project_info(). + * + * Overrides interface translation settings for locale_test module. + */ +function locale_test_locale_translation_additional_project_info() { + $projects['locale_test'] = array( + 'type' => 'module', + 'info' => array( + 'project' => 'locale_test', + 'interface translation server pattern' => 'core/modules/locale/test/modules/locale_test/%project-%version.%language.po', + ), + ); + + return $projects; +} 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 0000000..9c0c04f --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.info @@ -0,0 +1,7 @@ +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 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 0000000..a80d9da --- /dev/null +++ b/core/modules/locale/tests/modules/locale_test_disabled/locale_test_disabled.module @@ -0,0 +1,6 @@ +