diff --git a/potx.drush.inc b/potx.drush.inc index c833331..9258c0b 100644 --- a/potx.drush.inc +++ b/potx.drush.inc @@ -61,6 +61,44 @@ function potx_drush_help($section) { } /** + * Find other paths to look for modules, based on the given folder's path. + * The returned paths are based on Drupal 8's folder structure, and include: + * /path/to/drupal/core/modules + * /sites/all/modules + * /profiles/ * /modules + * /modules/ + * + * @param string $folder + */ +function potx_drush_module_paths($folder) { + $folder = realpath($folder); + if (substr($folder, -1) != '/') { + $folder = $folder . '/'; + } + + $module_paths = array(); + $checks = array('/sites/', '/core/', '/profiles/', '/modules/'); + foreach ($checks as $check) { + if (preg_match("!$check!", $folder)) { + $parts = explode($check, $folder, 2); + $root = $parts[0]; + $module_paths[] = $root . '/sites/all/modules/'; + $module_paths[] = $root . '/core/modules/'; + + $profile_dirs = glob($root . '/profiles/*', GLOB_ONLYDIR); + if (is_array($profile_dirs)) { + foreach ($profile_dirs as $profile_dir) { + $module_paths[] = $profile_dir . '/modules/'; + } + } + + return array('core' => $root . '/core/', 'modules' => $module_paths); + } + } + + return array(); +} +/** * Drush command callback. */ function potx_drush_extract($mode = NULL) { @@ -68,6 +106,7 @@ function potx_drush_extract($mode = NULL) { include_once __DIR__ . '/potx.inc'; $files = array(); + $extra_paths = array(); $build_mode = POTX_BUILD_SINGLE; if (!is_null($mode) && in_array($mode, array('core', 'multiple', 'single'))) { @@ -97,6 +136,7 @@ function potx_drush_extract($mode = NULL) { } elseif (!empty($folder_option)) { $files = _potx_explore_dir($folder_option, '*', $api_option, TRUE); + $extra_paths = potx_drush_module_paths($folder_option); } else { // No file list provided so autodiscover files in current directory. @@ -108,7 +148,7 @@ function potx_drush_extract($mode = NULL) { _potx_process_file($file, 0, '_potx_save_string', '_potx_save_version', $api_option); } - potx_finish_processing('_potx_save_string', $api_option); + potx_finish_processing('_potx_save_string', $api_option, $extra_paths); _potx_build_files(POTX_STRING_RUNTIME, $build_mode); _potx_build_files(POTX_STRING_INSTALLER, POTX_BUILD_SINGLE, 'installer'); diff --git a/potx.inc b/potx.inc index 6ee0900..f51238a 100644 --- a/potx.inc +++ b/potx.inc @@ -382,11 +382,11 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po * @param string $save_callback * @param int $api_version */ -function potx_finish_processing($save_callback = '_potx_save_string', $api_version = POTX_API_CURRENT) { +function potx_finish_processing($save_callback = '_potx_save_string', $api_version = POTX_API_CURRENT, $extra_paths = array()) { global $yaml_translation_patterns; if ($api_version > POTX_API_7) { // Parsing shipped configuration has to happen after processing all schemas. - _potx_parse_shipped_configuration($save_callback, $api_version); + _potx_parse_shipped_configuration($save_callback, $api_version, $extra_paths); // Clear yaml translation patterns, so translation patterns for a module // won't be used for extracting translatable strings for another module. $yaml_translation_patterns = NULL; @@ -1984,35 +1984,54 @@ function _potx_load_yaml_translation_patterns($path) { */ function _potx_parse_yaml_file($code, $file_name, $file_path, $save_callback) { global $yaml_translation_patterns; - global $_potx_config_set; + global $_potx_module_metadata; if (!is_array($yaml_translation_patterns)) { _potx_init_yaml_translation_patterns(); } + try { + $yaml = Yaml::parse($code); + } catch (ParseException $e) { + watchdog('potx', "YAML parseing error on file @path: @error", array( + '@path' => $file_path, + '@error' => $e->getMessage(), + )); + + return; + } + foreach ($yaml_translation_patterns as $pattern => $trans_list) { if (fnmatch($pattern, $file_name) || fnmatch('*/' . $pattern, $file_name)) { - try { - $yaml = Yaml::parse($code); - _potx_find_yaml_translatables($yaml, $trans_list, $file_name, $save_callback); - } catch (ParseException $e) { - watchdog('potx', "YAML parseing error on file @path: @error", array( - '@path' => $file_path, - '@error' => $e->getMessage(), - )); - } + _potx_find_yaml_translatables($yaml, $trans_list, $file_name, $save_callback); } } if (preg_match('~config/schema/[^/]+\.yml$~', $file_name)) { - $schema = Yaml::parse($code); - foreach ($schema as $key => $element) { - _potx_process_config_schema($key, $element); - } + + $module_name = basename(dirname(dirname(dirname($file_name)))); + $_potx_module_metadata[$module_name]['schemas'][] = $file_name; } - elseif (preg_match('~config/(install|optional)/[^/]+\.yml$~', $file_name)) { - $_potx_config_set[] = $file_path; + elseif (preg_match('~config/(install|optional)/[^/]+\.yml$~', $file_name, $matches)) { + $module_name = basename(dirname(dirname(dirname($file_name)))); + // Store install and optional configs separately. + $_potx_module_metadata[$module_name]['configs-' . $matches[1]][] = $file_name; } + elseif (preg_match('~[^/]+.info.yml~', $file_name)) { + _potx_load_module_metadata($file_name, $yaml); + } +} + +/** + * Load a module's metadata from its .info.yml file. Drupal 8+. + * This metadata is used during dependency resolution. + */ +function _potx_load_module_metadata($info_path, $info_yaml) { + global $_potx_module_metadata; + + $module_name = substr(basename($info_path), 0, -9); + $_potx_module_metadata[$module_name]['dependencies'] = isset($info_yaml['dependencies']) ? $info_yaml['dependencies'] : array(); + $_potx_module_metadata[$module_name]['version'] = isset($info_yaml['version']) && $info_yaml['version'] != 'VERSION' ? $info_yaml['version'] : NULL; } /** @@ -2030,6 +2049,11 @@ function _potx_find_yaml_translatables($yaml, $trans_list, $file_name, $save_cal $trans_list['keys'] = array_diff($trans_list['keys'], array('%top-level-key')); } + // Don't try to process empty yaml files. + if (!is_array($yaml)) { + return; + } + foreach ($yaml as $key => $value) { if (in_array($key, $trans_list['keys'], TRUE)) { @@ -2131,18 +2155,7 @@ function _potx_find_yaml_translatables($yaml, $trans_list, $file_name, $save_cal */ function _potx_process_config_schema($schema_prefix, $schema_data) { global $_potx_processed_schema; - - if (!isset($_potx_processed_schema)) { - // The initial values for 'translatables' allows a limited support for - // extracting translatable strings from contrib projects, until - // https://www.drupal.org/node/1933988 gets in. - $_potx_processed_schema = array( - 'translatables' => array('label', 'text', 'date_format'), - 'types' => array(), - 'mappings' => array(), - 'contexts' => array('date_format' => 'PHP date format'), - ); - } + global $_potx_module_schema; // Elements can opt out of translation with a 'translatable: false' key. if (isset($schema_data['translatable']) && $schema_data['translatable'] === FALSE) { @@ -2167,14 +2180,18 @@ function _potx_process_config_schema($schema_prefix, $schema_data) { if ($type != 'mapping') { $_potx_processed_schema['types'][$schema_prefix] = $type; + $_potx_module_schema['types'][$schema_prefix] = $type; } $_potx_processed_schema['mappings'][] = $schema_prefix; + $_potx_module_schema['mappings'][] = $schema_prefix; } else { if ($type == 'sequence') { - _potx_process_config_schema($schema_prefix . '+sequence', $schema_data['sequence'][0]); + + _potx_process_config_schema($schema_prefix . '+sequence', $schema_data['sequence']); $_potx_processed_schema['types'][$schema_prefix] = 'sequence'; + $_potx_module_schema['types'][$schema_prefix] = 'sequence'; } // If the element's type is in the "translatables" list, or it has a // 'translatable: true' key, then it is translatable. @@ -2182,18 +2199,22 @@ function _potx_process_config_schema($schema_prefix, $schema_data) { || (isset($schema_data['translatable']) && $schema_data['translatable'] === TRUE)) { $_potx_processed_schema['translatables'][] = $schema_prefix; + $_potx_module_schema['translatables'][] = $schema_prefix; // Elements can define a context string, or inherit the context from their // defined type. if (isset($schema_data['translation context'])) { $_potx_processed_schema['contexts'][$schema_prefix] = $schema_data['translation context']; + $_potx_module_schema['contexts'][$schema_prefix] = $schema_data['translation context']; } elseif (isset($_potx_processed_schema['contexts'][$type])) { $_potx_processed_schema['contexts'][$schema_prefix] = $_potx_processed_schema['contexts'][$type]; + $_potx_module_schema['contexts'][$schema_prefix] = $_potx_processed_schema['contexts'][$type]; } } else { $_potx_processed_schema['types'][$schema_prefix] = $type; + $_potx_module_schema['types'][$schema_prefix] = $type; } } } @@ -2244,8 +2265,6 @@ function _potx_element_has_schema($type) { * block.* */ function _potx_find_matching_schema($name) { - global $_potx_processed_schema; - if (_potx_element_has_schema($name)) { return $name; } @@ -2273,6 +2292,31 @@ function _potx_find_matching_schema($name) { } /** + * Finds the possible schema keys that match with a config element name. + * + * @param string $name + * Name of the config element. + */ +function _potx_find_matching_schema_candidates($name) { + $candidates = array($name); + + $replaced = preg_replace('/([^.:]+)([.:*]*)$/', '*\2', $name); + if ($replaced != $name) { + $candidates[] = $replaced; + + $one_star = preg_replace('/\.([:.*]*)$/', '.*', $replaced); + if ($one_star != $replaced) { + $candidates[] = $one_star; + } + + $candidates = array_merge($candidates, _potx_find_matching_schema_candidates($replaced)); + $candidates = array_unique($candidates); + } + + return $candidates; +} + +/** * Replaces variables in configuration name. * * The configuration name may contain one or more variables to be replaced, @@ -2355,28 +2399,329 @@ function _potx_replace_variable($value, $data) { * @param string $save_callback * @param string $api_version */ -function _potx_parse_shipped_configuration($save_callback = '_potx_save_string', $api_version = POTX_API_CURRENT) { - global $_potx_config_set; - if (!is_array($_potx_config_set)) { +function _potx_parse_shipped_configuration($save_callback = '_potx_save_string', $api_version = POTX_API_CURRENT, $extra_paths = array()) { + global $_potx_module_metadata; + global $_potx_schema_cache; + global $_potx_processed_schema; + global $_potx_processed_modules; + + $_potx_schema_cache = array(); + + foreach ($_potx_module_metadata as $module_name => $module_metadata) { + if (!isset($module_metadata['configs-install']) && !isset($module_metadata['configs-optional'])) { + continue; + } + + $_potx_processed_modules = array(); + $_potx_processed_schema = array( + 'translatables' => array(), + 'types' => array(), + 'mappings' => array(), + 'contexts' => array() + ); + + _potx_process_module_schemas(array('core'), $extra_paths); + if (!isset($_potx_schema_cache['core'])) { + $_potx_schema_cache['core'] = $_potx_processed_schema; + } + _potx_process_module_schemas(array($module_name), $extra_paths); + if (!isset($_potx_schema_cache[$module_name])) { + $_potx_schema_cache[$module_name] = $_potx_processed_schema; + } + if (isset($module_metadata['configs-install'])) { + foreach ($module_metadata['configs-install'] as $config_path) { + + $module_path = dirname(dirname(dirname($config_path))); + + // Find the schema that matches the config file. + $path_info = pathinfo($config_path); + $config_name = $path_info['filename']; + + $config_code = file_get_contents($config_path); + $parsed_config = Yaml::parse($config_code); + unset($config_code); + + // Dependencies defined in a config file itself should not be visible + // to other config files. So, make a temp copy here, and revert after + // parsing the config file. + $temp = $_potx_processed_schema; + if (isset($parsed_config['dependencies']['module'])) { + _potx_process_module_schemas($parsed_config['dependencies']['module'], $extra_paths); + } + + $schema = _potx_find_matching_schema($config_name); + _potx_find_shipped_config_translatables($parsed_config, $schema, NULL, NULL, $config_path, $save_callback); + $_potx_processed_schema = $temp; + } + } + + if (isset($module_metadata['configs-optional'])) { + // Optional configs are different, since they do not explicitly specify + // which module they belong to. + foreach ($module_metadata['configs-optional'] as $config_path) { + + $_potx_processed_modules = array(); + $_potx_processed_schema = array( + 'translatables' => array(), + 'types' => array(), + 'mappings' => array(), + 'contexts' => array() + ); + + _potx_process_module_schemas(array('core'), $extra_paths); + + $module_path = dirname(dirname(dirname($config_path))); + + // Find the schema that matches the config file. + $path_info = pathinfo($config_path); + $config_name = $path_info['filename']; + + $config_code = file_get_contents($config_path); + $parsed_config = Yaml::parse($config_code); + unset($config_code); + + $match_candidates = _potx_find_matching_schema_candidates($config_name); + + // Search for modules that contain any schema definitions that could possibly match with this config file. + $result = db_query("SELECT * FROM {potx_schema_reverse_lookup} WHERE schema_key IN (:match_candidates)", array(':match_candidates' => $match_candidates)); + $mapped = array(); + foreach ($result as $record) { + $mapped[$record->schema_key] = $record->module_name; + } + + foreach ($match_candidates as $candidate) { + if (isset($mapped[$candidate])) { + $base_module = $mapped[$candidate]; + + _potx_process_module_schemas(array($base_module), $extra_paths); + if (!isset($_potx_schema_cache[$base_module])) { + $_potx_schema_cache[$base_module] = $_potx_processed_schema; + } + + break; + } + } + + if (isset($parsed_config['dependencies']['module'])) { + _potx_process_module_schemas($parsed_config['dependencies']['module'], $extra_paths); + } + + $schema = _potx_find_matching_schema($config_name); + _potx_find_shipped_config_translatables($parsed_config, $schema, NULL, NULL, $config_path, $save_callback); + } + } + } +} + +/** + * Recursively process and merge the schemas required for parsing shipped config. + * Fetch the schemas from cache, database, or the local filesystem. + * Handles module dependencies, and config optional dependencies. + * + * @param array $extra_paths + * Extra paths to look for modules, if they are not found in the cache or + * database. This is useful when potx is run locally (e.g. drush), and the + * path to a single module is given by the user. + */ +function _potx_process_module_schemas($module_list, $extra_paths = array()) { + global $_potx_module_metadata; + global $_potx_schema_cache; + global $_potx_processed_modules; + + // Remove modules that have already been processed. + $module_list = array_diff($module_list, $_potx_processed_modules); + + // If a module's schemas is found in the cache, use that. Dependency schemas + // are already merged in the cache. + foreach ($module_list as $module_name) { + if (isset($_potx_schema_cache[$module_name])) { + $_potx_processed_modules[] = $module_name; + unset($module_list[array_search($module_name, $module_list)]); + _potx_merge_processed_schema($_potx_schema_cache[$module_name]); + } + } + + if (count($module_list) == 0) { return; } - foreach ($_potx_config_set as $config_path) { - // Find the schema that matches the config file. - $path_info = pathinfo($config_path); - $config_name = $path_info['filename']; - $schema = _potx_find_matching_schema($config_name); + // Mark as processed early, to prevent a loop while traversing dependency graph. + $_potx_processed_modules = array_merge($_potx_processed_modules, $module_list); - if ($schema == '') { - // No schema was found matching this config. - continue; + $dependencies = array(); + + // Fetch processed schema for modules found in the database, and remove them from $module_list. + $result = db_query("SELECT * FROM {potx_parsed_data} WHERE module_name IN (:module_names)", array(':module_names' => $module_list)); + foreach ($result as $record) { + $record->dependencies = unserialize($record->dependencies); + + // Processed schemas for a module in database do not include dependency schemas. + // Merge the list of dependencies for all modules into $dependencies, and fetch them later. + if (is_array($record->dependencies)) { + $dependencies = array_merge($dependencies, $record->dependencies); + } + if (_potx_schema_needs_rebuild($record)) { + _potx_store_module_schema($record->module_name, $record); + } + else { + $record->parsed_schema = unserialize($record->parsed_schema); + _potx_merge_processed_schema($record->parsed_schema); + } + + unset($module_list[array_search($record->module_name, $module_list)]); + } + + // For remaining modules that are not found in database or cache: + foreach ($module_list as $module_name) { + // If the module is already found in the local filesystem, merge its dependecy list. + if (isset($_potx_module_metadata[$module_name])) { + if (!empty($_potx_module_metadata[$module_name]['dependencies'])) { + $dependencies = array_merge($dependencies, $_potx_module_metadata[$module_name]['dependencies']); + } + } + else if (!empty($extra_paths)) { + // Look for the module and its schemas in the given extra paths. + $found = FALSE; + + if ($module_name == 'core') { + $module_files = _potx_explore_dir($extra_paths['core'] . 'config/schema/', '*', POTX_API_8); + foreach ($module_files as $file_name) { + if (preg_match('~config/schema/[^/]+\.yml$~', $file_name)) { + $_potx_module_metadata['core']['schemas'][] = $file_name; + } + } + + $found = TRUE; + } + else { + foreach($extra_paths['modules'] as $path) { + $info_path = $path . $module_name . '/' . $module_name . '.info.yml'; + if (file_exists($info_path)) { + + $code = file_get_contents($info_path); + try { + $yaml = Yaml::parse($code); + } catch (ParseException $e) { + watchdog('potx', "YAML parseing error on file @path: @error", array( + '@path' => $file_path, + '@error' => $e->getMessage(), + )); + + continue; + } + + _potx_load_module_metadata($info_path, $yaml); + $dependencies = array_merge($dependencies, $_potx_module_metadata[$module_name]['dependencies']); + + $module_files = _potx_explore_dir($path . $module_name . '/', '*', POTX_API_8); + foreach ($module_files as $file_name) { + + if (preg_match('~config/schema/[^/]+\.yml$~', $file_name)) { + $_potx_module_metadata[$module_name]['schemas'][] = $file_name; + } + } + + $found = TRUE; + break; + } + } + } + + if (!$found) { + $message = t('Module @module not found. Some shipped configuration translatable strings might be missed.', array('@module' => $module_name)); + potx_status('error', $message); + } + } + } + + $dependencies = array_unique($dependencies); + _potx_process_module_schemas($dependencies, $extra_paths); + + foreach ($module_list as $module_name) { + if (isset($_potx_module_metadata[$module_name])) { + _potx_store_module_schema($module_name); } + } +} + +/** + * Detects if the processed schema stored in the database for a module needs to be rebuilt. + * Dev modules are always rebuild, since they have no version information, + * and are constantly updated. + */ +function _potx_schema_needs_rebuild($record) { + global $_potx_module_metadata; + + // If the module is found in the filesystem, but it's a different version, or a dev version, it needs a rebuild. + return isset($_potx_module_metadata[$record->module_name]) && + (!isset($_potx_module_metadata[$record->module_name]['version']) || + ($_potx_module_metadata[$record->module_name]['version'] != $record->module_version)); +} + +/** + * Merge a module's processed schema with the global schema storage. + */ +function _potx_merge_processed_schema($schema) { + global $_potx_processed_schema; - $config_code = file_get_contents($config_path); - $parsed_config = Yaml::parse($config_code); - unset($config_code); + $_potx_processed_schema['translatables'] = array_merge($_potx_processed_schema['translatables'], $schema['translatables']); + $_potx_processed_schema['types'] = array_merge($_potx_processed_schema['types'], $schema['types']); + $_potx_processed_schema['mappings'] = array_merge($_potx_processed_schema['mappings'], $schema['mappings']); + $_potx_processed_schema['contexts'] = array_merge($_potx_processed_schema['contexts'], $schema['contexts']); +} + +/** + * Process a module's config schema files, and store them in the database. + * + * @param string $record + * The database record to update, if the module's schema is already stored in + * the database. + */ +function _potx_store_module_schema($module_name, $record = NULL) { + global $_potx_module_metadata; + global $_potx_module_schema; + + $_potx_module_schema = array( + 'translatables' => array(), + 'types' => array(), + 'mappings' => array(), + 'contexts' => array(), + ); + + $query = db_insert('potx_schema_reverse_lookup')->fields(array('schema_key', 'module_name')); + + if (is_array($_potx_module_metadata[$module_name]['schemas'])) { + foreach ($_potx_module_metadata[$module_name]['schemas'] as $file_path) { + $code = file_get_contents($file_path); + $yaml = Yaml::parse($code); + foreach ($yaml as $key => $element) { + _potx_process_config_schema($key, $element); + + $query->values(array( + 'schema_key' => $key, + 'module_name' => $module_name + )); + } + } + $query->execute(); + } + + if ($record == NULL) { + $record = new stdClass(); + $record->module_name = $module_name; + if ($module_name != 'core') { + $record->module_version = $_potx_module_metadata[$module_name]['version']; + $record->dependencies = $_potx_module_metadata[$module_name]['dependencies']; + } + + $record->parsed_schema = $_potx_module_schema; + + drupal_write_record('potx_parsed_data', $record); + } + else { + $record->parsed_schema = $_potx_module_schema; - _potx_find_shipped_config_translatables($parsed_config, $schema, NULL, NULL, $config_path, $save_callback); + drupal_write_record('potx_parsed_data', $record, array('pid')); } } diff --git a/potx.install b/potx.install index 0cbf947..ab5759f 100644 --- a/potx.install +++ b/potx.install @@ -29,3 +29,68 @@ function potx_requirements($phase) { return $requirements; } + +/** + * Implements hook_schema(). + */ +function potx_schema() { + $schema = array(); + + $schema['potx_parsed_data'] = array( + 'description' => 'Parsed data for modules (Drupal 8+)', + 'fields' => array( + 'pid' => array( + 'description' => 'Parse ID.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'module_name' => array( + 'description' => 'Module name.', + 'type' => 'varchar', + 'length' => '255', + 'not null' => TRUE, + ), + 'module_version' => array( + 'description' => 'Module version.', + 'type' => 'varchar', + 'length' => '32', + ), + 'dependencies' => array( + 'description' => 'The module\'s dependencies.', + 'type' => 'text', + 'size' => 'medium', + 'serialize' => TRUE + ), + 'parsed_schema' => array( + 'description' => 'The module\'s parsed schema.', + 'type' => 'text', + 'size' => 'big', + 'serialize' => TRUE + ), + ), + 'primary key' => array('pid'), + ); + $schema['potx_schema_reverse_lookup'] = array( + 'description' => 'A reverse lookup table for finding modules based on schema key', + 'fields' => array( + 'lid' => array( + 'description' => 'Lookup ID.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'schema_key' => array( + 'description' => 'The schema key', + 'type' => 'varchar', + 'length' => '500' + ), + 'module_name' => array( + 'description' => 'The array name', + 'type' => 'varchar', + 'length' => '500' + ) + ), + 'primary key' => array('lid') + ); + + return $schema; +} diff --git a/tests/drupal8/config/schema/test.sequences.schema.yml b/tests/drupal8/config/schema/test.sequences.schema.yml index 5f7ba6c..537c2c9 100644 --- a/tests/drupal8/config/schema/test.sequences.schema.yml +++ b/tests/drupal8/config/schema/test.sequences.schema.yml @@ -2,20 +2,20 @@ simple_sequence: type: sequence label: 'Test sequence' sequence: - - type: text - label: 'Test string in sequence' + type: text + label: 'Test string in sequence' typed_sequence: type: sequence sequence: - - type: simple_mapping + type: simple_mapping sequence_of_sequence: type: sequence sequence: - - type: sequence - sequence: - - type: mapping - mapping: - id: - type: integer - description: - type: text + type: sequence + sequence: + type: mapping + mapping: + id: + type: integer + description: + type: text diff --git a/tests/drupal8/config/schema/test.variables.schema.yml b/tests/drupal8/config/schema/test.variables.schema.yml index 1d2b714..d9fb11f 100644 --- a/tests/drupal8/config/schema/test.variables.schema.yml +++ b/tests/drupal8/config/schema/test.variables.schema.yml @@ -16,7 +16,7 @@ variable.plugin.*: basic_variable: type: sequence sequence: - - type: variable.plugin.[plugin_id] + type: variable.plugin.[plugin_id] variable.text: type: text @@ -34,7 +34,7 @@ parent_variable.*: key_variable: type: sequence sequence: - - type: variable.[%key] + type: variable.[%key] complex_variable: type: mapping diff --git a/tests/drupal8/core/config/schema/core.data_types.schema.yml b/tests/drupal8/core/config/schema/core.data_types.schema.yml new file mode 100644 index 0000000..79e3f14 --- /dev/null +++ b/tests/drupal8/core/config/schema/core.data_types.schema.yml @@ -0,0 +1,782 @@ +# Base types provided by Drupal core. + +# Read https://www.drupal.org/node/1905070 for more details about configuration +# schema, types and type resolution. + +# Undefined type used by the system to assign to elements at any level where +# configuration schema is not defined. Using explicitly has the same effect as +# not defining schema, so there is no point in doing that. +undefined: + label: 'Undefined' + class: '\Drupal\Core\Config\Schema\Undefined' + +# Explicit type to use when no data typing is possible. Instead of using this +# type, we strongly suggest you use configuration structures that can be +# described with other structural elements of schema, and describe your schema +# with those elements. +ignore: + label: 'Ignore' + class: '\Drupal\Core\Config\Schema\Ignore' + +# Basic scalar data types from typed data. +boolean: + label: 'Boolean' + class: '\Drupal\Core\TypedData\Plugin\DataType\BooleanData' +email: + label: 'Email' + class: '\Drupal\Core\TypedData\Plugin\DataType\Email' +integer: + label: 'Integer' + class: '\Drupal\Core\TypedData\Plugin\DataType\IntegerData' +float: + label: 'Float' + class: '\Drupal\Core\TypedData\Plugin\DataType\FloatData' +string: + label: 'String' + class: '\Drupal\Core\TypedData\Plugin\DataType\StringData' +uri: + label: 'Uri' + class: '\Drupal\Core\TypedData\Plugin\DataType\Uri' + +# Container data types for lists with known and unknown keys. +mapping: + label: Mapping + class: '\Drupal\Core\Config\Schema\Mapping' + definition_class: '\Drupal\Core\TypedData\MapDataDefinition' +sequence: + label: Sequence + class: '\Drupal\Core\Config\Schema\Sequence' + definition_class: '\Drupal\Core\TypedData\ListDataDefinition' + +# Simple extended data types: + +# Human readable string that must be plain text and editable with a text field. +label: + type: string + label: 'Label' + translatable: true + +# String containing plural variants, separated by EXT. +plural_label: + type: label + label: 'Plural variants' + +# Internal Drupal path +path: + type: string + label: 'Path' + +# Human readable string that can contain multiple lines of text or HTML. +text: + type: string + label: 'Text' + translatable: true + +# PHP Date format string that is translatable. +date_format: + type: string + label: 'Date format' + translatable: true + translation context: 'PHP date format' + +# HTML color value. +color_hex: + type: string + label: 'Color' + +# Complex extended data types: + +# Root of a configuration object. +config_object: + type: mapping + mapping: + langcode: + type: string + label: 'Language code' + +# Mail text with subject and body parts. +mail: + type: mapping + label: 'Mail' + mapping: + subject: + type: label + label: 'Subject' + body: + type: text + label: 'Body' + +# Filter with module and status. +filter: + type: mapping + label: 'Filter' + mapping: + id: + type: string + label: 'ID' + provider: + type: string + label: 'Provider' + status: + type: boolean + label: 'Status' + weight: + type: integer + label: 'Weight' + settings: + type: filter_settings.[%parent.id] + +# System action configuration base. +action_configuration_default: + type: sequence + label: 'Action configuration' + sequence: + type: string + +theme_settings: + type: config_object + mapping: + favicon: + type: mapping + label: 'Shortcut icon settings' + mapping: + mimetype: + type: string + label: 'MIME type' + path: + type: string + label: 'Path' + url: + type: string + label: 'URL' + use_default: + type: boolean + label: 'Use the default shortcut icon supplied by the theme' + features: + type: mapping + label: 'Optional features' + mapping: + comment_user_picture: + type: boolean + label: 'User pictures in comments' + comment_user_verification: + type: boolean + label: 'User verification status in comments' + favicon: + type: boolean + label: 'Shortcut icon' + logo: + type: boolean + label: 'Logo' + name: + type: boolean + label: 'Site name' + node_user_picture: + type: boolean + label: 'User pictures in posts' + slogan: + type: boolean + label: 'Site slogan' + logo: + type: mapping + label: 'Logo settings' + mapping: + path: + type: string + label: 'Logo path' + url: + type: uri + label: 'URL' + use_default: + type: boolean + label: 'Use default' + third_party_settings: + type: sequence + label: 'Third party settings' + sequence: + type: theme_settings.third_party.[%key] + +views_field_bulk_form: + type: views_field + label: 'Bulk operation' + mapping: + action_title: + type: label + label: 'Action title' + include_exclude: + type: string + label: 'Available actions' + selected_actions: + type: sequence + label: 'Available actions' + sequence: + type: string + label: 'Action' + +# Array of routes with route_name and route_params keys. +route: + type: mapping + label: 'Route' + mapping: + route_name: + type: string + label: 'Route Name' + route_params: + type: sequence + label: 'Route Params' + sequence: + type: string + label: 'Param' + +# Config dependencies. +config_dependencies_base: + type: mapping + mapping: + config: + type: sequence + label: 'Configuration entity dependencies' + sequence: + type: string + content: + type: sequence + label: 'Content entity dependencies' + sequence: + type: string + module: + type: sequence + label: 'Module dependencies' + sequence: + type: string + theme: + type: sequence + label: 'Theme dependencies' + sequence: + type: string + +config_dependencies: + type: config_dependencies_base + label: 'Configuration dependencies' + mapping: + enforced: + type: config_dependencies_base + label: 'Enforced configuration dependencies' + +config_entity: + type: mapping + mapping: + uuid: + type: string + label: 'UUID' + langcode: + type: string + label: 'Language code' + status: + type: boolean + label: 'Status' + dependencies: + type: config_dependencies + label: 'Dependencies' + third_party_settings: + type: sequence + label: 'Third party settings' + sequence: + type: '[%parent.%parent.%type].third_party.[%key]' + +block_settings: + type: mapping + label: 'Block settings' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Description' + label_display: + type: string + label: 'Display title' + cache: + type: mapping + label: 'Cache settings' + mapping: + max_age: + type: integer + label: 'Maximum age' + status: + type: boolean + label: 'Status' + info: + type: label + label: 'Admin info' + view_mode: + type: string + label: 'View mode' + provider: + type: string + label: 'Provider' + +condition.plugin: + type: mapping + label: 'Condition' + mapping: + id: + type: string + label: 'ID' + negate: + type: boolean + label: 'Negate' + uuid: + type: string + label: 'UUID' + context_mapping: + type: sequence + label: 'Context assignments' + sequence: + type: string + +display_variant.plugin: + type: mapping + label: 'Display variant' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + weight: + type: integer + label: 'Weight' + uuid: + type: string + label: 'UUID' + +base_entity_reference_field_settings: + type: mapping + mapping: + target_type: + type: string + label: 'Type of item to reference' + target_bundle: + type: string + label: 'Bundle of item to reference' + +field_config_base: + type: config_entity + mapping: + id: + type: string + label: 'ID' + field_name: + type: string + label: 'Field name' + entity_type: + type: string + label: 'Entity type' + bundle: + type: string + label: 'Bundle' + label: + type: label + label: 'Label' + description: + type: text + label: 'Help text' + required: + type: boolean + label: 'Required field' + translatable: + type: boolean + label: 'Translatable' + default_value: + type: sequence + label: 'Default values' + sequence: + type: field.value.[%parent.%parent.field_type] + label: 'Default value' + default_value_callback: + type: string + label: 'Default value callback' + settings: + type: field.field_settings.[%parent.field_type] + field_type: + type: string + label: 'Field type' + +core.base_field_override.*.*.*: + type: field_config_base + label: 'Base field bundle override' + +core.date_format.*: + type: config_entity + label: 'Date format' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + locked: + type: boolean + label: 'Locked' + pattern: + type: date_format + label: 'PHP date format' + +# Generic field settings schemas. + +field.storage_settings.*: + type: mapping + label: 'Settings' + +field.field_settings.*: + type: mapping + label: 'Settings' + +field.value.*: + type: mapping + label: 'Default value' + +# Schema for the configuration of the String field type. + +field.storage_settings.string: + type: mapping + label: 'String settings' + mapping: + max_length: + type: integer + label: 'Maximum length' + case_sensitive: + type: boolean + label: 'Case sensitive' + is_ascii: + type: boolean + label: 'Contains US ASCII characters only' + +field.field_settings.string: + type: mapping + label: 'String settings' + +field.value.string: + type: mapping + label: 'Default value' + mapping: + value: + type: string + label: 'Value' + +# Schema for the configuration of the String (long) field type. + +field.storage_settings.string_long: + type: mapping + label: 'String (long) settings' + mapping: + case_sensitive: + type: boolean + label: 'Case sensitive' + +field.field_settings.string_long: + type: mapping + label: 'String (long) settings' + +field.value.string_long: + type: mapping + label: 'Default value' + mapping: + value: + type: text + label: 'Value' + +# Schema for the configuration of the URI field type. + +field.storage_settings.uri: + type: mapping + label: 'URI settings' + mapping: + max_length: + type: integer + label: 'Maximum length' + case_sensitive: + type: boolean + label: 'Case sensitive' + +field.field_settings.uri: + type: mapping + label: 'URI settings' + +field.value.uri: + type: mapping + label: 'Default value' + mapping: + value: + type: string + label: 'Value' + +# Schema for the configuration of the Created field type. + +field.storage_settings.created: + type: mapping + label: 'Created timestamp settings' + +field.field_settings.created: + type: mapping + label: 'Created timestamp settings' + +field.value.created: + type: mapping + label: 'Default value' + mapping: + value: + type: integer + label: 'Value' + +# Schema for the configuration of the Changed field type. + +field.storage_settings.changed: + type: mapping + label: 'Changed timestamp settings' + +field.field_settings.changed: + type: mapping + label: 'Changed timestamp settings' + +field.value.changed: + type: mapping + label: 'Default value' + mapping: + value: + type: integer + label: 'Value' + +# Schema for the configuration of the Entity reference field type. + +field.storage_settings.entity_reference: + type: mapping + label: 'Entity reference field storage settings' + mapping: + target_type: + type: string + label: 'Type of item to reference' + +field.field_settings.entity_reference: + type: mapping + label: 'Entity reference field settings' + mapping: + handler: + type: string + label: 'Reference method' + handler_settings: + type: entity_reference_selection.[%parent.handler] + label: 'Entity reference selection plugin settings' + +field.value.entity_reference: + type: mapping + label: 'Default value' + mapping: + target_id: + type: string + label: 'Value' + target_uuid: + type: string + label: 'Target UUID' + +# Schema for the configuration of the Boolean field type. + +field.field_settings.boolean: + label: 'Boolean settings' + type: mapping + mapping: + on_label: + type: string + label: 'On label' + off_label: + type: string + label: 'Off label' + +field.value.boolean: + type: mapping + mapping: + value: + type: integer + label: 'Value' + +# Schema for the configuration of the Email field type. + +field.storage_settings.email: + type: mapping + label: 'Email settings' + +field.field_settings.email: + type: mapping + label: 'Email settings' + sequence: + type: string + label: 'Setting' + +field.value.email: + type: mapping + label: 'Default value' + mapping: + value: + type: email + label: 'Value' + +# Schema for the configuration of the Integer field type. + +field.storage_settings.integer: + type: mapping + label: 'Integer settings' + mapping: + unsigned: + type: boolean + label: 'Unsigned' + size: + type: string + label: 'Database storage size' + +field.field_settings.integer: + type: mapping + label: 'Integer' + mapping: + min: + type: integer + label: 'Minimum' + max: + type: integer + label: 'Maximum' + prefix: + type: label + label: 'Prefix' + suffix: + type: label + label: 'Suffix' + +field.value.integer: + type: mapping + label: 'Default value' + mapping: + value: + type: integer + label: 'Value' + +# Schema for the configuration of the Decimal field type. + +field.storage_settings.decimal: + type: mapping + label: 'Decimal settings' + mapping: + precision: + type: integer + label: 'Precision' + scale: + type: integer + label: 'Scale' + +field.field_settings.decimal: + type: mapping + label: 'Decimal settings' + mapping: + min: + type: float + label: 'Minimum' + max: + type: float + label: 'Maximum' + prefix: + type: label + label: 'Prefix' + suffix: + type: label + label: 'Suffix' + +field.value.decimal: + type: mapping + label: 'Default value' + mapping: + value: + type: float + label: 'Value' + +# Schema for the configuration of the Float field type. + +field.storage_settings.float: + type: mapping + label: 'Float settings' + +field.field_settings.float: + type: mapping + label: 'Float settings' + mapping: + min: + type: float + label: 'Minimum' + max: + type: float + label: 'Maximum' + prefix: + type: label + label: 'Prefix' + suffix: + type: label + label: 'Suffix' + +field.value.float: + type: mapping + label: 'Default value' + mapping: + value: + type: float + label: 'Value' + +# Text with a text format. +text_format: + type: mapping + label: 'Text with text format' + # We declare the entire mapping of text and text format as translatable. This + # causes the entire mapping to be saved to the language overrides of the + # configuration. Storing only the (to be formatted) text could result in + # security problems in case the text format of the source text is changed. + translatable: true + mapping: + value: + type: text + label: 'Text' + # Mark the actual text as translatable (in addition to the entire mapping + # being marked as translatable) so that shipped configuration with + # formatted text can participate in the string translation system. + translatable: true + format: + type: string + label: 'Text format' + # The text format should not be translated as part of the string + # translation system, so this is not marked as translatable. + +# Schema for the configuration of the Entity reference selection plugins. + +entity_reference_selection: + type: mapping + label: 'Entity reference selection plugin configuration' + mapping: + target_bundles: + type: sequence + label: 'types' + sequence: + type: string + label: 'Type' + sort: + type: mapping + label: 'Sort settings' + mapping: + field: + type: string + label: 'Sort by' + direction: + type: string + label: 'Sort direction' + auto_create: + type: boolean + label: 'Create referenced entities if they don''t already exist' + +entity_reference_selection.*: + type: entity_reference_selection diff --git a/tests/drupal8/drupal8.info.yml b/tests/drupal8/drupal8.info.yml new file mode 100644 index 0000000..23df0e1 --- /dev/null +++ b/tests/drupal8/drupal8.info.yml @@ -0,0 +1,3 @@ +name: 'Drupal 8 Test' +type: module +description: 'Test module for Drupal 8 translatable string extraction.' diff --git a/tests/potx.test b/tests/potx.test index 87b5eda..c52ca10 100644 --- a/tests/potx.test +++ b/tests/potx.test @@ -403,7 +403,7 @@ class PotxTestCase extends DrupalWebTestCase { $this->buildOutput(POTX_API_8); - for ($i = 1; $i < 8; $i++) { + for ($i = 2; $i < 8; $i++) { $this->assertNoMsgID($i . ''); }