diff --git a/core/modules/config_translation/config_translation.api.php b/core/modules/config_translation/config_translation.api.php index 8878c34..fb3ebf4 100644 --- a/core/modules/config_translation/config_translation.api.php +++ b/core/modules/config_translation/config_translation.api.php @@ -11,53 +11,43 @@ */ /** - * Introduce translation tabs for in-place translation of configuration. + * Introduce dynamic translation tabs for translation of configuration. * - * This hook is used to gather a list of configuration name groups and entity - * mappers tied to paths to introduce translation tabs at various places in - * core for in-place translation editing on configuration. + * This hook augments MODULE.config_translation.yml as well as + * THEME.config_translation.yml files to collect dynamic translation mapper + * information. If your information is static, just provide such a YAML file + * with your module containing the mapping. * - * @return array - * A mapping of configuration name groups to paths and other options, - * encapsulated in \Drupal\config_translation\ConfigGroupMapper instances or - * \Drupal\config_translation\ConfigEntityMapper instances. + * This hook is also useful to extend existing mappers with new configuration + * names, for example when altering existing forms with new settings stored + * elsewhere. This allows the translation experience to also reflect the + * compound form element in one screen. * - * @see config_translation_menu(). - */ -function hook_config_translation_group_info() { - $items = array(); - - // Provide menu path for base editing page, title configuration page, - // array of configuration names to encompass. Optionally provide the created - // menu item type and whether to include an additional edit tab. - $items[] = new ConfigGroupMapper('admin/config/development/myconfig', t('My module main configuration'), array('mymodule.config_key')); - $items[] = new ConfigGroupMapper('admin/config/development/otherconfig', t('My module additional settings'), array('mymodule.other_key', 'mymodule.yet_another_key'), MENU_LOCAL_TASK, TRUE); - - // Provide menu path with path position for entity placeholder, entity - // machine name, title with @label placeholder for the entity and - // configuration name prefix for storage. Optionally provide the created menu - // item type and whether to include an additional edit tab. - $items[] = new ConfigEntityMapper('admin/config/development/myconfig/list/%myentity', 5, 'myentity', t('@label my entity'), 'myentity.entity'); - return $items; -} - -/** - * Alter any configuration mappers that have been set up. - * - * Useful to extend existing groups with new configuration names, for example - * when altering existing forms with new settings stored elsewhere. This allows - * the translation experience to also reflect the compound form element in one - * screen. + * @param array $info + * An associative array of discovered configuration mappers. Use an entity + * name for the key (for entity mapping) or a unique string for configuration + * name list mapping. The values of the associative array are arrays + * themselves in the same structure as the *.configuration_translation.yml + * files. * - * @param array $config_mappers - * Array of configuration mappers to alter. The array is keyed by paths for - * each group (unlike in hook_config_translation_group_info()), for easier - * implementation in both hooks. + * @see \Drupal\config_translation\Routing\RouteSubscriber::routes() + * @see config_translation_menu() */ -function hook_config_translation_group_info_alter(array &$config_mappers) { - $path = 'admin/config/development/myconfig'; - if (isset($config_mappers[$path])) { - $config_mappers[$path]->addName('othermodule.altered_settings_key'); +function hook_config_translation_info_alter(&$info) { + // Add fields entity mappers to all fieldable entity types defined. + $entity_manager = Drupal::entityManager(); + foreach ($entity_manager->getDefinitions() as $entity_type => $entity_info) { + // Make sure entity type is fieldable and has base path. + if ($entity_info['fieldable'] && isset($entity_info['route_base_path'])) { + $info[$entity_type . '_fields'] = array( + 'type' => 'entity', + 'base_path' => $entity_info['route_base_path'] . '/fields/{field_instance}', + 'entity_type' => 'field_instance', + 'title' => t('@label field'), + 'menu_item_type' => 'MENU_CALLBACK', + 'add_edit_tab' => '0', + ); + } } } diff --git a/core/modules/config_translation/config_translation.config_translation.yml b/core/modules/config_translation/config_translation.config_translation.yml new file mode 100644 index 0000000..83da0b0 --- /dev/null +++ b/core/modules/config_translation/config_translation.config_translation.yml @@ -0,0 +1,120 @@ +block: + type: entity + base_path: 'admin/structure/block/manage/{block}' + entity_type: block + title: '@label block' + +custom_block_type: + type: entity + base_path: 'admin/structure/custom-blocks/manage/{custom_block_type}' + entity_type: custom_block_type + title: '@label custom block type' + +contact_category: + type: entity + base_path: 'admin/structure/contact/manage/{contact_category}' + entity_type: contact_category + title: '@label contact category' + +node_type: + type: entity + base_path: 'admin/structure/types/manage/{node_type}' + entity_type: node_type + title: '@label content type' + menu_item_type: 'MENU_CALLBACK' + +date_format: + type: entity + base_path: 'admin/config/regional/date-time/formats/manage/{date_format}' + entity_type: date_format + title: '@label date format' + +filter_format: + type: entity + base_path: 'admin/config/content/formats/manage/{filter_format}' + entity_type: filter_format + title: '@label text format' + add_edit_tab: '1' + +image_style: + type: entity + base_path: 'admin/config/media/image-styles/manage/{image_style}' + entity_type: image_style + title: '@label image style' + +language_entity: + type: entity + base_path: 'admin/config/regional/language/edit/{language_entity}' + entity_type: language_entity + title: '@label image style' + menu_item_type: 'MENU_CALLBACK' + +menu: + type: entity + base_path: 'admin/structure/menu/manage/{menu}' + entity_type: menu + title: '@label menu' + +picture_mapping: + type: entity + base_path: 'admin/config/media/picturemapping/{picture_mapping}' + entity_type: picture_mapping + title: '@label picture mapping' + +shortcut_set: + type: entity + base_path: 'admin/config/user-interface/shortcut/manage/{shortcut_set}' + entity_type: shortcut_set + title: '@label shortcut set' + +taxonomy_vocabulary: + type: entity + base_path: 'admin/structure/taxonomy/manage/{taxonomy_vocabulary}' + entity_type: taxonomy_vocabulary + title: '@label vocabulary' + +user_role: + type: entity + base_path: 'admin/people/roles/manage/{user_role}' + entity_type: user_role + title: '@label user role' + add_edit_tab: '1' + +view: + type: entity + base_path: 'admin/structure/views/view/{view}' + entity_type: view + title: '@label user role' + menu_item_type: 'MENU_CALLBACK' + +maintenance: + type: names + base_path: 'admin/config/development/maintenance' + names: + - 'system.maintenance' + title: 'System maintenance' + add_edit_tab: '1' + +site_information: + type: names + base_path: 'admin/config/system/site-information' + names: + - 'system.site' + title: 'System information' + add_edit_tab: '1' + +rss_publishing: + type: names + base_path: 'admin/config/services/rss-publishing' + names: + - 'system.rss' + title: 'RSS publishing' + add_edit_tab: '1' + +people: + type: names + base_path: 'admin/config/people/accounts' + names: + - 'user.settings' + - 'user.mail' + title: 'Account settings' diff --git a/core/modules/config_translation/config_translation.module b/core/modules/config_translation/config_translation.module index f57d61a..6cff4ff 100644 --- a/core/modules/config_translation/config_translation.module +++ b/core/modules/config_translation/config_translation.module @@ -7,8 +7,6 @@ use Drupal\Core\Config\Schema\Element; use Drupal\Core\Language\Language; -use Drupal\config_translation\ConfigGroupMapper; -use Drupal\config_translation\ConfigEntityMapper; /** * Implements hook_help(). @@ -30,10 +28,10 @@ function config_translation_menu() { // Generate translation tabs for keys where a specific path can be // determined. This makes it possible to use translation features in-context // of the administration experience. - $config_groups = config_translation_get_groups(); - foreach ($config_groups as $group) { - $path = $group->getBasePath(); - if ($group->needsEditTab()) { + $mappers = drupal_container()->get('plugin.manager.config_translation')->getMappers(); + foreach ($mappers as $mapper) { + $path = $mapper->getBasePathPattern(); + if ($mapper->needsEditTab()) { // For pages that do not have a default tab, we need a default local task // on this level, so that the translate tab will show up. $items[$path . '/edit'] = array( @@ -44,8 +42,8 @@ function config_translation_menu() { } $items[$path . '/translate'] = array( 'title' => 'Translate', - 'route_name' => $group->getRouteName(), - 'type' => $group->getMenuItemType(), + 'route_name' => $mapper->getRouteName(), + 'type' => $mapper->getMenuItemType(), 'weight' => 100, ); } @@ -165,94 +163,24 @@ function config_translation_exists($name, Language $language) { } /** - * Gets group definitions from hooks and make it possible to alter groups. - * - * Configuration groups are used to get multiple configuration names used for - * one specific configuration form together. If contributed modules alter a - * form adding in additional settings stored elsewhere, the list of names - * can be expanded. - * - * @return array - * An array of configuration groups. - */ -function config_translation_get_groups() { - $config_groups = Drupal::ModuleHandler()->invokeAll('config_translation_group_info'); - - // Create an array of path indexed groups for easier altering. - $path_indexed_groups = array(); - foreach ($config_groups as $group) { - $path_indexed_groups[$group->getBasePath()] = $group; - } - Drupal::ModuleHandler()->alter('config_translation_group_info', $path_indexed_groups); - return $path_indexed_groups; -} - -/** - * Implements hook_config_translation_group_info(). - * - * Returns configuration name group mappings for core configuration files. + * Implements hook_config_translation_info_alter(). */ -function config_translation_config_translation_group_info() { - $items = array(); - - // Block. - $items[] = new ConfigEntityMapper('admin/structure/block/manage/{block}', 'block', t('@label block')); - - // Custom block. - $items[] = new ConfigEntityMapper('admin/structure/custom-blocks/manage/{custom_block_type}', 'custom_block_type', t('@label custom block type')); - - // Contact. - $items[] = new ConfigEntityMapper('admin/structure/contact/manage/{contact_category}', 'contact_category', t('@label contact category')); - - // Content types. - $items[] = new ConfigEntityMapper('admin/structure/types/manage/{node_type}', 'node_type', t('@label content type'), MENU_CALLBACK); - - // Date format. - $items[] = new ConfigEntityMapper('admin/config/regional/date-time/formats/manage/{date_format}', 'date_format', t('@label date format')); - - // Fields. +function config_translation_config_translation_info_alter(&$info) { + // Add fields entity mappers to all fieldable entity types defined. $entity_manager = Drupal::entityManager(); foreach ($entity_manager->getDefinitions() as $entity_type => $entity_info) { // Make sure entity type is fieldable and has base path. if ($entity_info['fieldable'] && isset($entity_info['route_base_path'])) { - $items[] = new ConfigEntityMapper($entity_info['route_base_path'] . '/fields/{field_instance}' , 'field_instance', t('@label field'), MENU_CALLBACK); + $info[$entity_type . '_fields'] = array( + 'type' => 'entity', + 'base_path' => $entity_info['route_base_path'] . '/fields/{field_instance}', + 'entity_type' => 'field_instance', + 'title' => t('@label field'), + 'menu_item_type' => 'MENU_CALLBACK', + 'add_edit_tab' => '0', + ); } } - - // Filter. - $items[] = new ConfigEntityMapper('admin/config/content/formats/manage/{filter_format}', 'filter_format', t('@label text format'), MENU_LOCAL_TASK, TRUE); - - // Images. - $items[] = new ConfigEntityMapper('admin/config/media/image-styles/manage/{image_style}', 'image_style', t('@label image style'), MENU_LOCAL_TASK); - - // Language. - $items[] = new ConfigEntityMapper('admin/config/regional/language/edit/{language_entity}', 'language_entity', t('@label language'), MENU_CALLBACK); - - // Menu. - $items[] = new ConfigEntityMapper('admin/structure/menu/manage/{menu}', 'menu', t('@label menu')); - - // Picture. - $items[] = new ConfigEntityMapper('admin/config/media/picturemapping/{picture_mapping}', 'picture_mapping', t('@label picture mapping'), MENU_LOCAL_TASK); - - // Shortcut. - $items[] = new ConfigEntityMapper('admin/config/user-interface/shortcut/manage/{shortcut_set}', 'shortcut_set', t('@label shortcut set')); - - // System. - $items[] = new ConfigGroupMapper('admin/config/development/maintenance', t('System maintenance'), array('system.maintenance'), MENU_LOCAL_TASK, TRUE); - $items[] = new ConfigGroupMapper('admin/config/system/site-information', t('Site information'), array('system.site'), MENU_LOCAL_TASK, TRUE); - $items[] = new ConfigGroupMapper('admin/config/services/rss-publishing', t('RSS publishing'), array('system.rss'), MENU_LOCAL_TASK, TRUE); - - // Taxonomy. - $items[] = new ConfigEntityMapper('admin/structure/taxonomy/manage/{taxonomy_vocabulary}', 'taxonomy_vocabulary', t('@label vocabulary')); - - // User. - $items[] = new ConfigGroupMapper('admin/config/people/accounts', t('Account settings'), array('user.settings', 'user.mail')); - $items[] = new ConfigEntityMapper('admin/people/roles/manage/{user_role}', 'user_role', t('@label user role'), MENU_LOCAL_TASK, TRUE); - - // Views. - $items[] = new ConfigEntityMapper('admin/structure/views/view/{view}', 'view', t('@label view'), MENU_CALLBACK); - - return $items; } /** diff --git a/core/modules/config_translation/config_translation.services.yml b/core/modules/config_translation/config_translation.services.yml index 68fc96a..f1099eb 100644 --- a/core/modules/config_translation/config_translation.services.yml +++ b/core/modules/config_translation/config_translation.services.yml @@ -1,9 +1,13 @@ services: config_translation.subscriber: class: Drupal\config_translation\Routing\RouteSubscriber + arguments: ['@plugin.manager.config_translation'] tags: - { name: event_subscriber } config_translation.access_check: class: Drupal\config_translation\Access\ConfigNameCheck tags: - { name: access_check } + plugin.manager.config_translation: + class: Drupal\config_translation\ConfigMapperManager + arguments: ['@cache.cache', '@language_manager', '@module_handler'] diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php index e068181..d2c49ab 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php @@ -28,14 +28,12 @@ public function applies(Route $route) { */ public function access(Route $route, Request $request) { $mapper = $route->getDefault('mapper'); - $entity = $request->attributes->get($mapper->getType()); - // Get configuration group for this mapper. - $group = $mapper->getConfigGroup($entity); - $group_language = $group->getLanguageWithFallback(); - $language = NULL; + $mapper->populateFromRequest($request); + $source_language = $mapper->getLanguageWithFallback(); + $target_language = NULL; if ($request->query->has('langcode')) { - $language = language_load($request->query->get('langcode')); + $target_language = language_load($request->query->get('langcode')); } // Only allow access to translate configuration, if proper permissions are @@ -46,10 +44,10 @@ public function access(Route $route, Request $request) { // a good idea. return ( user_access('translate configuration') && - $group->hasSchema() && - $group->hasTranslatable() && - !$group_language->locked && - (empty($language) || (!$language->locked && $language->id != $group_language->id)) + $mapper->hasSchema() && + $mapper->hasTranslatable() && + !$source_language->locked && + (empty($target_language) || (!$target_language->locked && $target_language->id != $source_language->id)) ); } diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php index bed5a50..8c5348b 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php @@ -8,150 +8,87 @@ namespace Drupal\config_translation; use Drupal\Core\Language\Language; -use Drupal\config_translation\ConfigGroupMapper; +use Symfony\Component\HttpFoundation\Request; /** * Configuration entity mapper. */ -class ConfigEntityMapper implements ConfigMapperInterface { - - /** - * Base path for entity editing. - * - * @var string - */ - private $basePath = ''; - - /** - * Title for translation editing screen. - * - * @var string - */ - private $title = ''; +class ConfigEntityMapper extends ConfigNamesMapper { /** * Configuration entity type name. * * @var string */ - private $entityType = ''; + protected $entityType = ''; /** - * Menu item type. + * Loaded entity instance to help produce the translation interface. * - * @var int + * @var object */ - private $menuItemType = MENU_LOCAL_TASK; - - /** - * Indicator whether an edit tab should be added. - * - * @var bool - */ - private $addEditTab = FALSE; - - /** - * Router name for this group in the routing system. - * - * @var string - */ - private $routeName; - - /** - * Constructor for configuration group. - * - * @param string $base_path - * Base path to attach the group user interface to. - * @param string $entity_type - * Configuration entity type name. - * @param string $title - * Title for translation editing screen. Use @label for entity - * label placement. - * @param int $menu_item_type - * (optional) Menu item type to add. - * @param bool $add_edit_tab - * (optional) Indicator whether an edit tab should be added. If there are - * no existing tabs on the page, for the translation tab to show up, an - * edit tab will need to be added. - */ - function __construct($base_path, $entity_type, $title, $menu_item_type = MENU_LOCAL_TASK, $add_edit_tab = FALSE) { - $this->basePath = $base_path; - $this->entityType = $entity_type; - $this->title = $title; - $this->menuItemType = $menu_item_type; - $this->addEditTab = $add_edit_tab; - $this->setRouteName(); - } - - /** - * {@inheritdoc} - */ - public function needsEditTab() { - return $this->addEditTab; - } - - /** - * {@inheritdoc} - */ - public function getBasePath() { - return $this->basePath; - } + protected $entity; /** * {@inheritdoc} */ - public function getMenuItemType() { - return $this->menuItemType; + public function populateFromRequest(Request $request) { + $entity = $request->attributes->get($this->entityType); + $this->setEntity($entity); } /** - * {@inheritdoc} + * Sets the entity instance for this mapper. + * + * This method can only be invoked when the concrete entity is known, that is + * in a request for an entity translation path. After this method is called, + * the mapper is fully populated with the proper display title and + * configuration names to use to serve to check permissions or display a + * translation screen. */ - public function getConfigGroup($arg = NULL) { - // In this implementation, $arg is the configuration entity loaded via the - // menu (for contact module for example) or a string (configuration entity - // ID), in which case we need to load the entity (for views for example). - if (!isset($arg)) { - return NULL; - } - if (is_string($arg)) { - $entity = entity_load($this->entityType, $arg); - } - else { - $entity = $arg; + public function setEntity($entity) { + if (isset($this->entity)) { + return FALSE; } - // Replace path segment with the ID of the entity for further processing. - $base_path = str_replace('{' . $this->entityType . '}', $entity->id(), $this->basePath); + $this->entity = $entity; // Replace entity label in template title. - $title = format_string($this->title, array('@label' => $entity->label())); + $this->title = t($this->title, array('@label' => $entity->label())); - // The list of configuration IDs belonging to this entity. + // Add the list of configuration IDs belonging to this entity. We add on a + // possibly existing list of names. This allows modules to alter the entity + // page with more names if form altering added more configuration to an + // entity. This is not a Drupal 8 best practice (ideally the configuration + // would have pluggable components), but this may happen as well. $entity_type_info = entity_get_info($this->entityType); - $names = array($entity_type_info['config_prefix'] . '.' . $entity->id()); + $this->names = array_merge($this->names, array($entity_type_info['config_prefix'] . '.' . $entity->id())); - return new ConfigGroupMapper($base_path, $title, $names, $this->menuItemType, $this->addEditTab); + return TRUE; } /** * {@inheritdoc} */ - public function setRouteName() { - $search = array('/', '-', '{', '}'); - $replace = array('.', '_', '_', '_'); - $this->routeName = 'config_translation.item.' . str_replace($search, $replace, $this->basePath); + public function getBasePath() { + return str_replace('{' . $this->entityType . '}', $this->entity->id(), $this->basePathPattern); } /** - * {@inheritdoc} + * Set entity type for this mapper. + * + * This should be set in initialization. A mapper that knows its type but + * not yet its names is still useful for router item and tab generation. The + * concrete entity only turns out later with actual controller invocations, + * when the setEntity() method is invoked before the rest of the methods are + * used. */ - public function getRouteName() { - return $this->routeName; + public function setType($entity_type) { + $this->entityType = $entity_type; } /** - * {@inheritdoc} + * Get entity type from this mapper. */ public function getType() { return $this->entityType; diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php deleted file mode 100644 index 91b4da1..0000000 --- a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php +++ /dev/null @@ -1,72 +0,0 @@ - 'MENU_LOCAL_TASK', + 'add_edit_tab' => '0', + ); + + public function __construct(CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) { + + // Look at all themes and modules. + $directories = array(); + foreach ($module_handler->getModuleList() as $module => $filename) { + $directories[$module] = dirname($filename); + } + foreach (list_themes() as $theme_id => $theme) { + $directories[$theme->name] = drupal_get_path('theme', $theme->name); + } + + // Check for files named MODULE.config_translation.yml and + // THEME.config_translation.yml in module/theme roots. + $this->discovery = new YamlDiscovery('config_translation', $directories); + $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery); + $this->factory = new ContainerFactory($this); + // Let others alter definitions with hook_config_translation_info_alter(). + $this->alterInfo($module_handler, 'config_translation_info'); + $this->setCacheBackend($cache_backend, $language_manager, 'config_translation_info'); + } + + /** + * Returns a list of all mappers found. + */ + public function getMappers() { + $mappers = array(); + foreach($this->getDefinitions() as $definition) { + if ($definition['type'] == 'entity') { + $mapper = new ConfigEntityMapper( + $definition['base_path'], + $definition['title'], + array(), + constant($definition['menu_item_type']), + $definition['add_edit_tab'] + ); + $mapper->setType($definition['entity_type']); + } + else { + $mapper = new ConfigNamesMapper( + $definition['base_path'], + $definition['title'], + $definition['names'], + constant($definition['menu_item_type']), + $definition['add_edit_tab'] + ); + } + $mappers[$definition['base_path']] = $mapper; + } + return $mappers; + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php similarity index 51% rename from core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php rename to core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php index 44c83aa..be597a5 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php @@ -2,69 +2,70 @@ /** * @file - * Contains \Drupal\config_translation\ConfigGroupMapper. + * Contains \Drupal\config_translation\ConfigNamesMapper. */ namespace Drupal\config_translation; use Drupal\Core\Language\Language; +use Symfony\Component\HttpFoundation\Request; /** - * Configuration editing screen mapper. + * Configuration mapper base implementation. */ -class ConfigGroupMapper implements ConfigMapperInterface { +class ConfigNamesMapper { /** - * Base path for group to attach to. + * Base path for the mapper to attach to. * * @var string */ - private $basePath = ''; + protected $basePath = ''; /** * Title for translation editing screen. * * @var string */ - private $title = ''; + protected $title = ''; /** * List of configuration names. * * @var array */ - private $names = array(); + protected $names = array(); /** * Menu item type. * * @var int */ - private $menuItemType = MENU_LOCAL_TASK; + protected $menuItemType = MENU_LOCAL_TASK; /** * Indicator whether an edit tab should be added. * * @var bool */ - private $addEditTab = FALSE; + protected $addEditTab = FALSE; /** - * Router name for this group in the routing system. + * Router name for this mapper in the routing system. * * @var string */ - private $routeName; + protected $routeName; /** - * Constructor for configuration group. + * Constructor for configuration names mapper. * - * @param string $base_path - * Base path to attach the group user interface to. + * @param string $base_path_pattern + * Base path pattern to attach the translation user interface to. * @param string $title * Title for translation editing screen. * @param array $names - * List of configuration names in group. + * List of configuration names for this mapper. * @param int $menu_item_type * (optional) Menu item type to add. * @param bool $add_edit_tab @@ -72,66 +73,94 @@ class ConfigGroupMapper implements ConfigMapperInterface { * no existing tabs on the page, for the translation tab to show up, an * edit tab will need to be added. */ - function __construct($base_path, $title, $names, $menu_item_type = MENU_LOCAL_TASK, $add_edit_tab = FALSE) { - $this->basePath = $base_path; + function __construct($base_path_pattern, $title, $names = array(), $menu_item_type = MENU_LOCAL_TASK, $add_edit_tab = FALSE) { + $this->basePathPattern = $base_path_pattern; $this->title = $title; $this->names = $names; $this->menuItemType = $menu_item_type; $this->addEditTab = $add_edit_tab; - $this->setRouteName(); + + // Set route name based on base path. + $search = array('/', '-', '{', '}'); + $replace = array('.', '_', '_', '_'); + $this->routeName = 'config_translation.item.' . str_replace($search, $replace, $base_path_pattern); + } + + /** + * Populate the config mapper with request data. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * Page request object. + */ + public function populateFromRequest(Request $request) { + // A name mapper is fully populated without request data. } /** - * {@inheritdoc} + * Returns whether this configuration mapper needs an edit tab. + * + * @return bool + * TRUE if this mapper needs an edit tab, FALSE otherwise. */ public function needsEditTab() { return $this->addEditTab; } /** - * {@inheritdoc} + * Returns base path pattern of this configuration mapper. + * + * @return string + * The mapper base path pattern. + */ + public function getBasePathPattern() { + return $this->basePathPattern; + } + + /** + * Returns processed base path of this configuration mapper. + * + * If the path pattern contains an entity reference for example, the entity ID + * is replaced. + * + * @return string + * The mapper base path. */ public function getBasePath() { - return $this->basePath; + return $this->basePathPattern; } /** - * Returns title of this configuration group. + * Returns title of this translation page. * * @return string - * The group title. + * The page title. */ public function getTitle() { return $this->title; } /** - * Returns list of names in this configuration group. + * Returns list of names in this mapper. * * @return array - * An array of configuration names in this group. + * An array of configuration names in this mapper. */ public function getNames() { return $this->names; } /** - * {@inheritdoc} + * Returns the menu item type to be used for this mapper. + * + * @return int + * The menu item type used for this mapper. */ public function getMenuItemType() { return $this->menuItemType; } /** - * {@inheritdoc} - */ - public function getConfigGroup($arg = NULL) { - // This is a ConfigGroupMapper already. - return $this; - } - - /** - * Returns the original language code of the configuration group. + * Returns the original language code of the configuration. * * @todo Revisit. This assumes the langcode of the first element for now and * might lead to inconsistency. @@ -141,11 +170,11 @@ public function getLangcode() { } /** - * Returns language object for the configuration group. + * Returns language object for the configuration. * - * If the language of the group is not a configured language on the site and - * it is English, we return a dummy language object to represent the - * built-in language. + * If the language of the configuration files is not a configured language on + * the site and it is English, we return a dummy language object to represent + * the built-in language. * * @return \Drupal\Core\Language\Language * A configured language object instance or a dummy English language object. @@ -160,10 +189,10 @@ public function getLanguageWithFallback() { } /** - * Returns an array with configuration data for the group. + * Returns an array with all configuration data. * * @return array - * Configuration data keyed by configuration names in the group. + * Configuration data keyed by configuration names. */ public function getConfigData() { $config_data = array(); @@ -174,10 +203,10 @@ public function getConfigData() { } /** - * Checks that all pieces of this configuration group has schema. + * Checks that all pieces of this configuration mapper has schema. * * @return bool - * TRUE if all of the group elements have schema, FALSE otherwise. + * TRUE if all of the elements have schema, FALSE otherwise. */ public function hasSchema() { foreach ($this->names as $name) { @@ -189,10 +218,11 @@ public function hasSchema() { } /** - * Checks that all pieces of this configuration group have translatables. + * Checks that all pieces of this configuration mapper have translatables. * * @return bool - * TRUE if all of the group elements have translatables, FALSE otherwise. + * TRUE if all of the configuration elements have translatables, FALSE + * otherwise. */ public function hasTranslatable() { foreach ($this->names as $name) { @@ -204,14 +234,14 @@ public function hasTranslatable() { } /** - * Checks whether there is already a translation for this group. + * Checks whether there is already a translation for this mapper. * * @param \Drupal\Core\Language\Language $language * A language object. * * @return bool - * TRUE if any of the group elements have a translation in the given - * language, FALSE otherwise. + * TRUE if any of the configuration elements have a translation in the + * given language, FALSE otherwise. */ public function hasTranslation(Language $language) { foreach ($this->names as $name) { @@ -223,7 +253,7 @@ public function hasTranslation(Language $language) { } /** - * Adds the given configuration name to the list of names in the group. + * Adds the given configuration name to the list of names. * * @param string $name * Configuration name. @@ -233,24 +263,13 @@ public function addName($name) { } /** - * {@inheritdoc} - */ - public function setRouteName() { - $this->routeName = 'config_translation.item.' . str_replace(array('/', '-'), array('.', '_'), $this->basePath); - } - - /** - * {@inheritdoc} + * Returns route name for the mapper. + * + * @return string + * Route name for the mapper. */ public function getRouteName() { return $this->routeName; } - /** - * {@inheritdoc} - */ - public function getType() { - return NULL; - } - } diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php index bf07df0..da5b6ff 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php @@ -8,10 +8,9 @@ namespace Drupal\config_translation\Controller; use Drupal\Core\Config\Config; -use Drupal\Core\Controller\ControllerInterface; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Language\Language; -use Drupal\config_translation\ConfigGroupMapper; -use Drupal\config_translation\ConfigMapperInterface; +use Drupal\config_translation\ConfigNamesMapper; use Drupal\config_translation\Form\ConfigTranslationDeleteForm; use Drupal\config_translation\Form\ConfigTranslationManageForm; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,7 +19,7 @@ /** * Provides page callbacks for the configuration translation interface. */ -class ConfigTranslationController implements ControllerInterface { +class ConfigTranslationController implements ContainerInjectionInterface { /** * {@inheritdoc} @@ -34,35 +33,35 @@ public static function create(ContainerInterface $container) { * * @param \Symfony\Component\HttpFoundation\Request $request * Page request object. - * @param \Drupal\config_translation\ConfigMapperInterface $mapper + * @param \Drupal\config_translation\ConfigNamesMapper $mapper * Configuration mapper. * * @return array * Page render array. */ - public function itemOverviewPage(Request $request, ConfigMapperInterface $mapper) { + public function itemPage(Request $request, ConfigNamesMapper $mapper) { if ($request->query->has('action') && $request->query->has('langcode') ) { $action = $request->query->get('action'); - $langcode = $request->query->get('langcode'); + $target_language = language_load($request->query->get('langcode')); switch ($action) { case 'add': case 'edit': - return $this->itemTranslatePage($request, $action, $mapper); + return $this->itemTranslatePage($action, $mapper, $target_language); break; case 'delete': - return $this->itemDeletePage($request, $mapper); + return $this->itemDeletePage($mapper, $target_language); break; } } - $group = $this->getConfigGroup($request, $mapper); - drupal_set_title(t('Translations for %label', array('%label' => $group->getTitle())), PASS_THROUGH); + $page = array(); + $page['#title'] = t('Translations for %label', array('%label' => $mapper->getTitle())); // It is possible the original language this configuration was saved with is // not on the system. For example, the configuration shipped in English but // the site has no English configured. Represent the original language in // the table even if it is not currently configured. $languages = language_list(); - $original_langcode = $group->getLangcode(); + $original_langcode = $mapper->getLangcode(); if (!isset($languages[$original_langcode])) { $language_name = language_name($original_langcode); if ($original_langcode == 'en') { @@ -72,9 +71,8 @@ public function itemOverviewPage(Request $request, ConfigMapperInterface $mapper $languages[$original_langcode] = new Language(array('id' => $original_langcode, 'name' => $language_name)); } - $path = $group->getBasePath(); + $path = $mapper->getBasePath(); $header = array(t('Language'), t('Operations')); - $page = array(); $page['languages'] = array( '#type' => 'table', '#header' => $header, @@ -105,7 +103,7 @@ public function itemOverviewPage(Request $request, ConfigMapperInterface $mapper $operations = array(); $path_options = array('langcode' => $language->id); // If no translation exists for this language, link to add one. - if (!$group->hasTranslation($language)) { + if (!$mapper->hasTranslation($language)) { $operations['add'] = array( 'title' => t('Add'), 'href' => $path . '/translate', @@ -138,31 +136,28 @@ public function itemOverviewPage(Request $request, ConfigMapperInterface $mapper /** * Renders translation item manage form. * - * @param \Symfony\Component\HttpFoundation\Request $request - * Page request object. * @param string $action * Action identifier, either 'add' or 'edit'. Used to provide proper * labeling on the screen. - * @param \Drupal\config_translation\ConfigMapperInterface $mapper + * @param \Drupal\config_translation\ConfigNamesMapper $mapper * Configuration mapper. + * @param \Drupal\core\Language\Language $target_language + * Target language for translation page. * * @return array * The render array for the translation item manage form. */ - public function itemTranslatePage(Request $request, $action, ConfigMapperInterface $mapper) { - $group = $this->getConfigGroup($request, $mapper); - $language = $this->getLanguage($request, $mapper); - + public function itemTranslatePage($action, ConfigNamesMapper $mapper, Language $target_language) { $replacement = array( - '%label' => $group->getTitle(), - '@language' => strtolower($language->name), + '%label' => $mapper->getTitle(), + '@language' => $target_language->name, ); switch ($action) { case 'add': - drupal_set_title(t('Add @language translation for %label', $replacement), PASS_THROUGH); + $title = t('Add @language translation for %label', $replacement); break; case 'edit': - drupal_set_title(t('Edit @language translation for %label', $replacement), PASS_THROUGH); + $title = t('Edit @language translation for %label', $replacement); break; } @@ -174,66 +169,33 @@ public function itemTranslatePage(Request $request, $action, ConfigMapperInterfa // Get base language configuration to display in the form before entering // into the language context for the form. This avoids repetitively going // in and out of the language context to get original values later. - $base_config = $group->getConfigData(); + $base_config = $mapper->getConfigData(); // Leave override free context. config_context_leave(); // Enter context for the translation target language requested and generate // form with translation data in that language. - config_context_enter('Drupal\language\LanguageConfigContext')->setLanguage($language); + config_context_enter('Drupal\language\LanguageConfigContext')->setLanguage($target_language); $locale_storage = \Drupal::service('locale.storage'); - return drupal_get_form(new ConfigTranslationManageForm($locale_storage), $group, $language, $base_config); + $build = drupal_get_form(new ConfigTranslationManageForm($locale_storage), $mapper, $target_language, $base_config); + $build['#title'] = $title; + return $build; } /** * Renders the item delete form. * - * @param \Symfony\Component\HttpFoundation\Request $request - * Page request object. - * @param \Drupal\config_translation\ConfigMapperInterface $mapper + * @param \Drupal\config_translation\ConfigNamesMapper $mapper * Configuration mapper. + * @param \Drupal\core\Language\Language $target_language + * Language of translation to delete. * * @return array * The render array for the translation item delete form. */ - public function itemDeletePage(Request $request, ConfigMapperInterface $mapper) { - return drupal_get_form(new ConfigTranslationDeleteForm(), $this->getConfigGroup($request, $mapper), $this->getLanguage($request, $mapper)); - } - - /** - * Gets the configuration group. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * Page request object. - * @param \Drupal\config_translation\ConfigMapperInterface $mapper - * Configuration mapper. - * - * @return \Drupal\config_translation\ConfigGroupMapper - * The configuration group. - */ - protected function getConfigGroup(Request $request, ConfigMapperInterface $mapper) { - // Get configuration group for this mapper. - $entity = $request->attributes->get($mapper->getType()); - return $mapper->getConfigGroup($entity); - } - - /** - * Gets the language object. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * Page request object. - * @param \Drupal\config_translation\ConfigMapperInterface $mapper - * Configuration mapper. - * - * @return false|\Drupal\core\Language\Language - * Returns Language object when langcode found in request, FALSE otherwise. - */ - protected function getLanguage(Request $request, ConfigMapperInterface $mapper) { - if ($request->query->has('langcode')) { - return language_load($request->query->get('langcode')); - } - return FALSE; + public function itemDeletePage(ConfigNamesMapper $mapper, Language $target_language) { + return drupal_get_form(new ConfigTranslationDeleteForm(), $mapper, $target_language); } } diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php index a1d647b..e7d1f58 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php @@ -9,7 +9,7 @@ use Drupal\Core\Form\ConfirmFormBase; use Drupal\Core\Language\Language; -use Drupal\config_translation\ConfigMapperInterface; +use Drupal\config_translation\ConfigNamesMapper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; @@ -19,11 +19,11 @@ class ConfigTranslationDeleteForm extends ConfirmFormBase { /** - * The group of configuration translation to be deleted. + * The configuration translation to be deleted. * - * @var \Drupal\config_translation\ConfigMapperInterface + * @var \Drupal\config_translation\ConfigNamesMapper */ - protected $group; + protected $mapper; /** * The language of configuration translation. @@ -36,7 +36,7 @@ class ConfigTranslationDeleteForm extends ConfirmFormBase { * {@inheritdoc} */ public function getQuestion() { - t('Are you sure you want to delete the @language translation of %label?', array('%label' => $this->group->getTitle(), '@language' => $this->language->name)); + t('Are you sure you want to delete the @language translation of %label?', array('%label' => $this->mapper->getTitle(), '@language' => $this->language->name)); } /** @@ -50,7 +50,7 @@ public function getConfirmText() { * {@inheritdoc} */ public function getCancelPath() { - return $this->group->getBasePath() . '/translate'; + return $this->mapper->getBasePath() . '/translate'; } /** @@ -63,8 +63,8 @@ public function getFormID() { /** * {@inheritdoc} */ - public function buildForm(array $form, array &$form_state, ConfigMapperInterface $group = NULL, Language $language = NULL) { - $this->group = $group; + public function buildForm(array $form, array &$form_state, ConfigNamesMapper $mapper = NULL, Language $language = NULL) { + $this->mapper = $mapper; $this->language = $language; return parent::buildForm($form, $form_state); } @@ -74,14 +74,14 @@ public function buildForm(array $form, array &$form_state, ConfigMapperInterface */ public function submitForm(array &$form, array &$form_state) { $storage = drupal_container()->get('config.storage'); - foreach ($this->group->getNames() as $name) { + foreach ($this->mapper->getNames() as $name) { $storage->delete('locale.config.' . $this->language->id . '.' . $name); } // @todo Do we need to flush caches with drupal_flush_all_caches()? The // configuration change may affect page display. - drupal_set_message(t('@language translation of %label was deleted', array('%label' => $this->group->getTitle(), '@language' => $this->language->name))); - $form_state['redirect'] = $this->group->getBasePath() . '/translate'; + drupal_set_message(t('@language translation of %label was deleted', array('%label' => $this->mapper->getTitle(), '@language' => $this->language->name))); + $form_state['redirect'] = $this->mapper->getBasePath() . '/translate'; } } diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php index bad96f3..7e52365 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php @@ -11,7 +11,7 @@ use Drupal\Core\Config\Schema\Element; use Drupal\Core\Form\FormInterface; use Drupal\Core\Language\Language; -use Drupal\config_translation\ConfigMapperInterface; +use Drupal\config_translation\ConfigNamesMapper; use Drupal\locale\StringStorageInterface; /** @@ -20,11 +20,11 @@ class ConfigTranslationManageForm implements FormInterface { /** - * The group of the configuration translation. + * The mapper for configuration translation. * - * @var \Drupal\config_translation\ConfigMapperInterface + * @var \Drupal\config_translation\ConfigNamesMapper */ - protected $group; + protected $mapper; /** * The language of the configuration translation. @@ -76,20 +76,20 @@ public function getFormID() { * Builds configuration form with metadata and values from the source * language. * - * @param \Drupal\config_translation\ConfigMapperInterface $group - * The configuration group the form is being built for. + * @param \Drupal\config_translation\ConfigNamesMapper $mapper + * The configuration mapper the form is being built for. * @param Language $language * The language the form is adding or editing. * @param array $base_config_data * The base configuration in the source language. */ - public function buildForm(array $form, array &$form_state, ConfigMapperInterface $group = NULL, Language $language = NULL, array $base_config_data = NULL) { - $this->group = $group; + public function buildForm(array $form, array &$form_state, ConfigNamesMapper $mapper = NULL, Language $language = NULL, array $base_config_data = NULL) { + $this->mapper = $mapper; $this->language = $language; - $this->sourceLanguage = $this->group->getConfigGroup()->getLanguageWithFallback(); + $this->sourceLanguage = $this->mapper->getLanguageWithFallback(); $this->baseConfigData = $base_config_data; - foreach ($this->group->getNames() as $id => $name) { + foreach ($this->mapper->getNames() as $id => $name) { $form[$id] = array( '#type' => 'container', '#tree' => TRUE, @@ -118,7 +118,7 @@ public function submitForm(array &$form, array &$form_state) { // For the form submission handling, use the override free context. config_context_enter('config.context.free'); - foreach ($this->group->getNames() as $id => $name) { + foreach ($this->mapper->getNames() as $id => $name) { // Set configuration values based on form submission and source values. $base_config = config($name); $translation_config = config('locale.config.' . $this->language->id . '.' . $name); @@ -139,7 +139,7 @@ public function submitForm(array &$form, array &$form_state) { config_context_leave(); drupal_set_message(t('Updated @language configuration translations successfully.', array('@language' => $this->language->name))); - $form_state['redirect'] = $this->group->getBasePath() . '/translate'; + $form_state['redirect'] = $this->mapper->getBasePath() . '/translate'; } /** diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php b/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php index ad11867..54646d4 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php @@ -9,6 +9,7 @@ use \Drupal\Core\Routing\RouteBuildEvent; use \Drupal\Core\Routing\RoutingEvents; +use \Drupal\config_translation\ConfigMapperManager; use \Symfony\Component\EventDispatcher\EventSubscriberInterface; use \Symfony\Component\Routing\Route; @@ -18,6 +19,23 @@ class RouteSubscriber implements EventSubscriberInterface { /** + * The mapper plugin discovery service. + * + * @var \Drupal\config_translation\ConfigMapperManager + */ + protected $mapperManager; + + /** + * Constructs a new RouteSubscriber. + * + * @param \Drupal\config_translation\ConfigMapperManager $mapper_manager + * The mapper plugin discovery service. + */ + public function __construct(ConfigMapperManager $mapper_manager) { + $this->mapperManager = $mapper_manager; + } + + /** * {@inheritdoc} */ static function getSubscribedEvents() { @@ -40,18 +58,30 @@ static function getSubscribedEvents() { */ public function routes(RouteBuildEvent $event) { $collection = $event->getRouteCollection(); - $config_groups = config_translation_get_groups(); - foreach ($config_groups as $group) { - $path = $group->getBasePath(); + // Add configuration mappers. + $config_mappers = $this->mapperManager->getMappers(); + $this->generateRoutes($collection, $config_mappers); + } + + /** + * Generates routes for mappers. + * + * @param \Symfony\Component\Routing\RouteCollection $collection + * The route collection where the new dynamic routes are added. + * @param array $mappers + * An array of ConfigNamesMapper instances. + */ + protected function generateRoutes($collection, $mappers) { + foreach ($mappers as $mapper) { + $path = $mapper->getBasePathPattern(); $route = new Route($path . '/translate', array( - '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemOverviewPage', - 'mapper' => $group, + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage', + 'mapper' => $mapper, ),array( '_config_translation_config_name_access' => 'TRUE', )); - $collection->add($group->getRouteName(), $route); - + $collection->add($mapper->getRouteName(), $route); } } diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUITest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUITest.php index 4f17dd7..585b7ad 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUITest.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUITest.php @@ -69,7 +69,7 @@ public function setUp() { 'administer shortcuts', 'administer site configuration', 'administer taxonomy', - 'administer users', + 'administer account settings', 'administer languages', 'administer image styles', 'administer pictures', @@ -460,7 +460,7 @@ function doSettingsPageTest($link) { */ public function testTranslateOperationInListUI() { - // All lists from config_translation_config_translation_group_info(). + // All lists based on paths provided by the module. $this->doBlockListTest(); $this->doMenuListTest();